新消息:Helius 收购 Light Protocol
如何从 Ethereum 迁移到 Solana:开发者指南
博客/研究

如何从 Ethereum 迁移到 Solana:开发者指南

Developer Experience EngineerX 上的 0xIchigoLinkedIn 上的 0xIchigoGitHub 上的 0xIchigo
阅读需 41 分钟

本文讲了什么?

Ethereum 是近年来最重要的创新之一。人类历史上第一次拥有了一个用于社会协调的去中心化全球平台,它有潜力彻底改变众多行业。尽管 Ethereum 非常重要,但其运行时环境 Ethereum Virtual Machine(EVM)目前并不适合消费级应用。它是一个采用单线程、基于 gas 且费用波动较大的网络。相比之下,Solana 是一个高吞吐量、低延迟的网络。它提供并行化基础设施,费用低且可预测。它直接解决了 EVM 的局限并改进其原始设计,因此对于希望构建可扩展、高效应用的开发者而言,是极具吸引力的选择。

本文是一份面向有意在 Solana 上构建应用的 EVM 开发者的完整迁移指南。文章介绍两者之间的根本差异,包括 Ethereum 和 Solana 的共识机制、交易处理方式,以及智能合约开发所使用的语言。随后,文章会介绍 Solana 的账户模型,该模型为账户提供了更统一、更多面的设计。本文还会探讨 Solang 和 Neon EVM,这两款支持 Solidity 的工具可以改善 Solana 开发体验。

根本差异

本节将探讨 Ethereum 与 Solana 之间的关键差异。这两个区块链都旨在为全球协调创建去中心化状态机,但它们在共识机制、交易处理方式和智能合约开发语言方面存在显著差异。深入理解这些根本差异,有助于了解 Solana 作为高性能全球状态机相较于 Ethereum 的独特优势。

共识机制

共识机制是每个区块链网络的基础。它们负责决定如何验证交易,以及如何以安全、高效且去中心化的方式将区块添加到区块链中。Ethereum 和 Solana 都是权益证明(PoS)网络。虽然它们都以 PoS 为基础,但由于确认规则不同,实现共识的方式也有所差异。

Ethereum 使用 Gasper,它是 Casper 友好终局性组件(Casper-FFG)与 LMD-GHOST 分叉选择算法的组合。这一组合构成了保护 Ethereum 安全的共识机制。

Casper 是一种基于 PoS 的终局性系统,可将区块的确认状态提升为“已最终确定”。在 Ethereum 上,当占总质押量三分之二的投票支持纳入某个区块,并且另一个区块构建在它之上时,该区块就被视为已最终确定。由于终局性要求占总质押量三分之二的参与者同意某个区块属于规范链,攻击者若想创建另一条已最终确定的链,就必须拥有或操纵总质押量的三分之二,并通过罚没销毁至少三分之一。Casper 让新加入者可以放心地与规范链同步。 

LMD-GHOST 是“Latest Message-Driven Greedy Heaviest Observed Sub-Tree”的缩写。它是一种决定应跟随 Ethereum 哪条分叉的算法。该算法会选择获得网络验证者最多支持(即“权重”)的分叉,因此称为“贪心最重观察子树”。随后,它确保只考虑每位验证者发出的最新消息。每当 Ethereum 上有人提出新区块时,验证者都会使用这条规则决定该区块是否应成为规范链的一部分。

相比之下,Solana 使用历史证明(PoH)哈希来达成共识。尽管名称容易让人误解,但 PoH 并不是共识算法。它是一种在对抗性网络中证明时间的方式。更具体地说,它是一种加密时间戳函数,让节点无需相互通信即可就事件顺序达成一致。领导者(即向账本追加条目的验证者)会为区块添加时间戳,以证明自上一个区块以来已经过去了一段时间。 

添加这些时间戳会建立历史记录,因为它们可以证明数据在特定时间已经存在。历史记录通过连续的抗原像哈希函数创建,其中每个哈希都依赖前一个哈希。可验证延迟函数(VDF)在生成这些哈希时至关重要。它们确保每个哈希都建立在前一个哈希之上,并纳入从那时起经过的时间。集成 VDF 为哈希过程引入了时间维度,从而可以在 Solana 上创建可验证的带时间戳事件序列。

了解 Solana 的历史证明机制如何通过加密时间戳创建可靠的事件序列后,还必须理解它如何与 Solana 的共识机制 Tower BFT 集成。Tower 拜占庭容错(BFT)是 Solana 针对 PoH 优化的传统拜占庭容错共识模型变体。Tower BFT 使用 PoH 创建的历史记录作为参考框架。该框架让验证者能够高效、准确地对账本状态进行投票。在 Tower BFT 中,验证者的投票会被锁定很长时间,并且随着投票增加,承诺程度也会提高。借助 PoH,验证者无需相互通信,就能更快、更充分地判断账本状态。PoH 与 Tower BFT 的结合加快了共识,并增强了网络的安全性和可靠性。最终形成一个高吞吐量、可扩展的区块链环境,能够快速确认交易。

交易处理与执行环境

区块链的效率和可扩展性主要取决于其交易处理能力和执行环境。这些因素决定了交易的执行速度,以及网络操作的成本效率。区块链处理交易的方式及其执行环境的具体特性,会显著影响开发者体验。

Ethereum 的运行时环境是一个确定性的单线程堆栈机,称为 Ethereum Virtual Machine(EVM)。它的行为类似数学函数。也就是说,给定输入后,EVM 会生成确定性输出。可以将 Ethereum 定义为具有一个状态转换函数:f(S, T) = S’。给定旧状态(S)和一组新的有效交易(T),EVM 会生成新的有效输出状态(S’)。面对同一组交易,EVM 始终会得到相同的最终状态。这对于维持网络各节点之间的一致性至关重要。

EVM 按顺序处理交易。顺序处理可确保每笔交易的执行环境准确反映网络截至该时点的状态。顺序执行可以精确计算状态变化和 gas 成本。这种可预测性让开发者和用户能够清楚了解交易结果与成本。

Ethereum 采用单线程执行模型,简化了智能合约的执行环境。由于每笔交易都按顺序处理,开发者能够获得可预测的状态变化。可预测的状态变化可以说让 Ethereum 的开发环境对新开发者更加友好,因为他们能够将更多精力放在智能合约逻辑上,而不是复杂的执行机制上。然而,这种简单性也给可扩展性带来了挑战。顺序处理限制了网络吞吐量。在需求高峰期,这可能导致拥堵、交易等待时间延长和 gas 费用上涨。由于每笔交易都会消耗 gas,开发者不得不优化 gas 效率。这样做既能改善整体用户体验,也能应对拥堵;在拥堵期间,低效代码可能会让普通用户无法使用应用。

Solana 开发者无需像 Ethereum 开发者那样担心优化 gas 使用量。Solana 被设计为高吞吐量、低延迟的状态机,称为 Solana Virtual Machine(SVM)。SVM 的一个关键组件是 Sealevel。它是一个并行处理交易的运行时引擎。在 Solana 上,每笔交易都会向运行时说明执行时将读取或写入状态的哪些部分。运行时会并行处理互不冲突的交易,以及读取相同状态的交易。Sealevel 将交易工作负载分配到验证者硬件的多个线程,从而优化智能合约执行。因此,当验证者在一个核心上处理某笔交易时,另一个核心可以同时处理另一笔交易。

Solana 固有的并行性显著降低了交易成本。Solana 交易有两种费用:基础费用和优先费。每个签名的基础费用固定为 5000 lamports;大多数交易只有一个签名。优先费是可选的,允许交易相对于其他交易获得更高优先级。调度器会以非确定性方式优先处理优先费更高的交易。交易费用通常低于 0.001 美元,非投票交易的平均费用在 0.000005 至 0.00007 SOL 之间。由于近期优先费使用量增加,上限高于往常。按当前 SOL 价格约 ~98.96 美元计算,这笔费用约合 0.000494 至 0.006968 美元。 

这些费用同样可预测。Solana 使用本地化费用市场来管理需求。区块空间的结构可以防止任何单个活动热点(例如备受热捧的 NFT 铸造)占据区块空间并推高整个网络的费用。只有尝试访问特定高需求热点的交易才会面临费用上涨。本地化费用市场可以支持优先费,同时避免演变为全面的 gas 战争。这种本地化机制不同于 Ethereum 等基于 gas 的网络。在这些网络中,交易按顺序处理,全局拥堵会导致费用波动。

Ethereum 是一个单线程运行时环境,一次只能处理一个合约。它目前无法利用现代多核硬件,导致验证者硬件未被充分使用。为了让节点保持较低的硬件要求,Ethereum 的交易处理能力受到进一步限制。相比之下,Solana 的并行处理能力可以利用验证者的所有可用核心,从而处理更多交易。再加上本地化费用市场,Solana 成为了性能高得多的全球状态机。 

智能合约语言

Ethereum 智能合约开发的主要语言是 Solidity。它是一种静态类型、使用花括号的语言,专为创建在 EVM 上运行的智能合约而设计。它深受 JavaScript、C++ 和 Python 影响,因此熟悉这些语言的开发者很容易上手。

Yul 是一种针对 EVM 兼容区块链优化的中间低级语言。Yul 让开发者能够更深入地控制字节码执行,非常适合精细优化 gas 消耗和管理其他低级操作。开发者可以直接使用 Yul 编码,或将其作为编译目标,从而创建更高效的合约。

Vyper 是一种流行的 Python 风格语言,强调简洁性和安全性。它有意减少了相较于 Solidity 的功能,以降低复杂性和潜在安全漏洞。Vyper 的设计理念将可读性和可审计性置于首位。这种方式启发了 Fe 等类似语言,以及 Dasy 等编译为 Vyper 的语言。

Rust 是 Solana 智能合约(通常称为程序)开发的通用语言。它速度快、内存效率高,性能可与 C++ 媲美。这些特性使其适合为高吞吐量、低延迟网络开发应用。Rust 丰富的类型系统和所有权模型可保障内存与线程安全,促使开发者构建可靠、安全的应用。 

对于不熟悉系统编程的人来说,Rust 的学习曲线较陡,但作为回报,开发者可以创建稳健且高效的代码。大多数 Rust 开发都使用 Anchor。Anchor 是一个约定明确的框架,它通过减少样板代码、执行各种标准安全检查并简化序列化与反序列化流程,让程序开发变得更容易。因此,开始开发时并不需要掌握太多高级 Rust 知识。作为参考,Anchor 文档建议用户熟悉 Rust Book 的前九章(即熟悉 Rust 基础知识)。Solana Playground 提供了多篇 Anchor 教程,帮助你快速上手。

虽然 Rust 是首选,但开发者并不限于使用它。C、C++ 以及任何以 LLVM 的 BPF 后端为目标的语言(即任何可以编译为 BPF 字节码的语言)都可以使用。对于熟悉 Vyper 等 Python 风格语言的开发者,Seahorse Lang 允许使用 Python 编写程序。这样,开发者既能享受 Python 的易用性,又能获得与使用 Rust 编码相同的安全保障。你可以通过 Seahorse University 和 Seahorse Cookbook 等实用的 Seahorse 教程,立即开始在 Solana 上编程。

从 Ethereum 迁移的开发者并非只能使用 Rust。由于 Anchor 和 Seahorse 等框架广受欢迎且提供安全保障,我们建议学习和使用它们,但这并不总是可行。得益于 Solang 和 Neon Labs 最近在编译器及 EVM 兼容开发环境方面的进展,开发者可以使用 Solidity 开发程序。Solidity 在 Solana 上同样有一席之地。要开始使用 Solidity 开发程序,我们需要更深入地了解 Solana 编程模型的细节。对于进行这项迁移的开发者来说,理解 Solana 的账户模型至关重要,因为它是网络上程序开发与交互的基础。

理解账户模型

Ethereum 将账户分为两大类:外部拥有账户(EOA)和合约账户。EOA 是用户使用的标准账户类型。它们由私钥控制,可以持有 Ether 余额、发送交易并与合约账户交互。另一方面,合约账户的特殊之处在于,其操作由嵌入其中的智能合约代码控制。它们无法自行发起交易,只能响应来自 EOA 或其他合约账户的交易。

Solana 采用更统一的账户模型,将账户视为持久存储数据的多用途容器。该模型允许任何账户成为程序,模糊了账户与智能合约之间的传统界限。Ethereum 将代码和状态合并在同一个账户中,而 Solana 程序是无状态的。这意味着它们不会在内部存储任何状态。程序运行所需的所有数据都存储在单独的账户中,并通过交易以引用方式传入。通过引用传入账户后,只需部署一个通用程序,即可与不同账户交互。Solana 的账户模型将代码与数据分离,打造出更高效、更模块化的开发环境。对于希望与多个协议交互、又不想在不同程序之间移动资产的用户来说,这一点很有优势。在 Solana 上,一切都是账户。 

Solana 账户通常可以分为可执行账户和不可执行账户。简单来说,可执行账户是能够运行代码的账户。不可执行账户用于存储数据,无法执行代码,因为它们不存储任何代码。

可执行账户是存放程序的账户。程序可以拥有其他账户、读取其他账户或向其充值、修改数据,也可以从自己拥有的账户中扣款。可执行账户还可以进一步分为链上程序和原生程序。链上程序是用户编写并部署到网络的代码。其升级权限通常由部署程序的账户持有,该账户可以升级程序。原生程序是可执行账户的一个特殊子集,类似于 Ethereum 的预编译合约,但功能更加广泛。这些程序集成在 Solana 核心中,为验证者运行提供必要功能。原生程序的一个例子是系统程序。该程序负责创建新账户、分配账户数据、将账户分配给程序、从其拥有的账户中转移 lamports,以及支付交易费用。你可以在这里查看完整的原生程序列表。

无论是可执行账户还是不可执行账户,所有账户都具有相同的字段: 

  • lamports 字段,用于跟踪账户以 SOL 计价的原生余额
  • data 字段,表示账户存储的原始数据字节数组
  • owner 字段,表示可以修改该账户的程序
  • signer 字段,在交易中用于表示账户能否批准交易
  • writable 字段,用于指定账户数据能否修改。通过将交易中包含的账户标记为只读或可写,该字段有助于实现并行处理
  • executable 字段,用于表示账户是否存储程序
  • rent epoch 字段,用于表示账户下一次应支付租金的纪元

租金是维持 Solana 账户存续并确保其保存在验证者内存中的存储成本。因此,账户需要维持最低余额才能保持活跃。租金确保网络最终可以回收未使用或资金不足的账户,从而减少状态膨胀。近期更新后,主网上已不存在需要支付租金的账户。所有账户在创建时都必须免租。如果账户维持的最低余额相当于两年的租金,就可以免租。你可以使用 Test Drive 和 Solana CLI 的 rent 子命令等工具,估算账户免租所需的 SOL 数量。这与 Ethereum 的资源分配系统不同,后者的存储会一直保留,除非明确清除。Solana 的方案在减少状态膨胀的同时,为状态存储提供了更可预测的成本结构。

地址与程序派生地址(PDA)

所有账户都通过地址进行标识,地址是唯一的 32 字节公钥。这与 Ethereum 的数据模型不同,后者的地址为 20 字节。Solana 使用 ed25519(即一种采用 SHA-512 和 Curve22519 椭圆曲线的 EdDSA 签名方案)生成地址。账户必须是 ed25519 曲线上的点,才能拥有有效的密钥对。 

程序派生地址(PDA)是使用 bump(即将输出推离曲线的值)在曲线外生成的账户。PDA 需要三个主要部分:父程序的地址、一组 seeds 和一个 bump。seeds 是可以包含任意值的字符串数组。不过,大多数开发者会根据父程序中的状态变量创建特定 seeds,以构建类似 hashmap 的数据结构。因此,PDA 是通过 SHA-512 哈希函数对程序 ID、seeds 和 bump 进行哈希而创建的。

PDA 位于曲线外的目的是确保只有派生该 PDA 的程序才能代表 PDA 签名。这样可以通过程序化生成交易签名来简化交易流程,让无需信任的 dApp 在没有人工干预的情况下顺畅运行。

与账户交互

Ethereum 交易是由 EOA 发起的状态变更操作。智能合约无法独立发起交易,只能对交易作出响应。合约代码决定这些响应,交易执行成本以 gas 衡量。用户必须提供足够的 Ether 来支付这笔 gas 费用。

Ethereum 交易通常执行单项操作,例如调用特定的智能合约函数。虽然单笔交易可以在合约内引发多项状态变化,但所有变化都仅限于这一次合约调用的范围。

在 Solana 上,一笔交易由待读取或写入的账户数组、一个或多个签名,以及一条或多条指令组成。指令是调用单个程序的指示。它是执行逻辑的最小单位,也是最基本的操作单位。指令会指定执行程序、涉及的所有账户以及操作数据。程序解释指令中的数据,并对指定账户执行操作。这种结构让单笔交易能够以原子方式跨多个程序执行一系列操作。这意味着所有指令要么全部成功,要么全部失败。Solana 的交易流程不同于 Ethereum 模型,后者的交易通常与单个智能合约或 EOA 操作相关联。

跨程序调用(CPI)允许一个程序在同一笔交易中调用另一个程序。CPI 期间,每个计算单元计费的账户数据字节数为 250。按 200,000 个单元计算,大约为 50MB。CPI 允许在程序之间调用时使用以编程方式生成的签名。这类似于 Ethereum 智能合约高效且原子化地调用另一个合约。不过,调用程序会暂停,直到被调用程序处理完指令。重入仅限于直接自递归,固定深度上限为 4。这可以防止程序在中间状态调用另一个程序,却不知道自身稍后可能被再次调用的情况。

为提高效率,Solana 交易需要遵守大小限制,类似于 Ethereum 的 gas 限制,但其重点是数据大小。Solana 交易遵循 IPv6 最大传输单元(MTU)标准,以保证可靠的数据传输。预留必要的标头空间后,有 1232 字节可用于数据包数据。为突破大小限制,Solana 引入了版本化交易,以支持多种交易格式。除了旧版格式(即原始交易格式)之外,Solana 还发布了 Version 0,用于支持地址查找表(ALT)。ALT 将地址存储在链上的表状数据结构中,每个地址都使用 1 字节 u8 索引进行索引。这样可以显著缩小交易大小,因为每个账户只需要 1 字节,而不是 32 字节。

Solang

Solang 是面向 Solana 和 Polkadot 的 Solidity 编译器。它旨在兼容 EVM Solidity 编译器 0.8 版本的源文件,但也做了一些调整,以适配 Solana 和 Polkadot 的架构。Solang 的一个重要特性是使用了强大的编译器基础设施 LLVM。LLVM 的灵活性让 Solang 未来能够扩展对其他编程语言的支持,从而简化实现和编译。这一特性符合 Solang 的目标:帮助开发者更轻松地迁移到 Solana 或 Polkadot,并拓展 Solidity 的开发范围。Solang 正在持续开发,重点是提升兼容性、效率和易用性。对于希望将技能拓展到跨链领域的 Ethereum 开发者来说,了解 Solang 及其注意事项至关重要。

安装

安装 Solang 有几种方式。在 Mac 上,用户可以通过私有 tap 使用 Brew 下载 Solang:

代码
brew install hyperledger/solang/solang

另一种安装 Solang 的方式是使用 Solang 容器。如果你更喜欢使用 Docker,这种方式非常合适,因为新镜像会自动发布到这些容器中。目前提供 v.0.3.3 标签和 latest 标签:

代码
docker pull ghcr.io/hyperledger/solang:latest

安装和使用 Solang 较简单的方式之一是通过 Ancor。首先,确保系统已安装 Rust 和 Node.js。Windows 用户还需要设置 Windows Subsystem for Linux。接下来,我们需要安装 Solana 工具套件。在 Mac 和 Linux 上,可以使用以下命令:

代码
 sh -c "$(curl -sSfL https://release.solana.com/v1.17.13/install)"

如果你想使用其他软件版本,请将“v1.17.13”替换为相应的发布标签。你也可以使用以下符号化频道名称之一:stable、beta 或 edge。根据系统配置,你可能需要更新 PATH 环境变量。如果遇到此消息,请复制并粘贴下方推荐的命令来更新 PATH。运行 solana --version 可确认所需的 solana 版本。

在 Windows 上,以管理员身份打开命令提示符。复制并粘贴以下命令,将 Solana 安装程序下载到临时目录:

代码
cmd /c "curl https://release.solana.com/v1.17.13/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"

然后,复制并粘贴以下命令来安装最新版 Solana:C:\solana-install-tmp\solana-install-init.exe v1.17.13. 安装完成后,按 Enter。

关闭命令提示符窗口,然后以普通用户身份重新打开一个新窗口。运行 solana –-version,确认已安装所需版本的 solana。

接下来,安装 Anchor。

建议使用 Anchor 版本管理器(avm)安装 Anchor。可以通过 cargo 执行以下命令:

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

然后,安装并使用最新版本:

代码
avm install latest
avm use latest

# Verify the installation
avm –version

Anchor 0.28 版支持开发者直接使用 Solang 构建项目。开发者可以使用以下命令创建新的 Solang 项目:anchor init project_name —solidity。该命令会创建一个新的 Solang 程序和一个测试文件,用于演示如何通过客户端与程序交互。

如果你使用 Visual Studio Code,可以考虑安装 Solang 扩展来辅助语法高亮。请禁用其他所有已启用的 Solidity 扩展,确保 Solang 扩展正常运行。

创建新项目

使用命令 anchor-init project_name –solidity 创建新项目。该命令会以项目名称创建一个新目录。Solidity 标志用于告知 Anchor 我们希望使用 Solang。项目的 ./solidity 目录中会提供一个入门程序。合约如下所示:

代码
@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract test {
    bool private value = true;

    @payer(payer)
    constructor() {
        print("Hello, World!");
    }

    /// A message that can be called on instantiated contracts.
    /// This one flips the value of the stored `bool` from `true`
    /// to `false` and vice versa.
    function flip() public {
            value = !value;
    }

    /// Simply returns the current value of our `bool`.
    function get() public view returns (bool) {
            return value;
    }
}

此程序包含一个 constructor,用于创建程序、将状态变量 value 初始化为 true,并将“Hello, World!”写入程序日志。调用 flip 函数时,它会更新状态变量。get 函数会返回状态变量的当前值。除了少数注意事项外,它看起来就像普通的 Solidity 智能合约。最显著的区别在于注解的使用。

注解

注解用于账户管理。请注意 @program_id(“...”) 注解。如果事先知道程序的链上地址,可以使用它来指定该地址。如果你希望通过外部调用来调用合约,程序必须包含 @program_id 标记,或者使用 {program_id: … } 参数调用:

代码
@program_id(“...”);

contract Foo {
	function hello() public pure {
		print(“Hello”);
	}
}

contract Foo2 {
	function bye() public pure {
		print(“Bye”);
	}
}

contract Bar {
	function new_foo() external {
		Foo.new();
	}
}

contract Bar2 {
	function new_foo(address new_foo_id) external {
		Foo2.new{program_id: new_foo_id}();
	}
}

创建新项目时,提供的示例合约在构造函数上方带有 @payer 注解。该注解定义了负责支付程序数据账户初始化费用的账户。@payer(payer) 语法声明了一个名为 payer 的账户,每次调用构造函数时都需要该账户。

实例化合约时,需要一个程序账户来保存可执行代码,还需要一个数据账户来保存状态变量。数据账户可以使用客户端代码创建,然后传入调用构造函数的交易。也可以由构造函数创建数据账户。至少必须提供 @payer 注解。如果数据账户是 PDA,则必须提供 seed 和 bump。@seed 注解指定用于派生 PDA 的 seed,可以是字符串字面量,也可以是格式为 hex”1234” 的十六进制字符串。如果 seed 注解位于参数之前,它必须引用类型为 bytes、address 或定长字节数组的参数。@bump 注解指定用于生成非曲线地址的值。它必须是类型为 bytes1 的单字节值。可选的 @space 注解可用于指定数据账户的大小。它是一个 uint64 表达式,可以是常量,也可以使用构造函数的某个参数。根据 Solang 文档,@space 至少应为运行 solang -v 命令时给出的大小:

代码
$ solang compile --target solana -v examples/solana/flipper.sol
...
info: contract flipper uses at least 17 bytes account data
…

如果程序没有构造函数,可以将这些注解与空构造函数搭配使用。Solang 文档提供了以下示例:

代码
@program_id("Foo5mMfYo5RhRcWa4NZ2bwFn4Kdhe8rNK5jchxsKrivA")
contract Foo {

    @space(500 + 12)
    @seed("Foo")
    @payer(payer)
    constructor(@seed bytes seed_val, @bump bytes1 bump_val) {
        // ...
    }
}

函数注解用于声明外部函数所需的账户:

  • @account(foo) 将 foo 账户声明为只读账户
  • @mutableAccount(bar) 将 bar 账户声明为可变账户
  • @signer(fizz) 将 fizz 账户声明为只读签名者
  • @mutableSigner(buzz) 将 buzz 账户声明为可变签名者

可以在构造函数内部访问通过构造函数上的 @payer 注解声明的账户。通过函数注解声明的账户可在 tx.accounts 向量中使用。例如,可以使用 tx.accounts.ichigo 访问声明为 @account(ichigo) 的账户。它会返回 AccountInfo 内置结构体。该结构体与我们在“理解账户模型”一节中介绍的结构相同,只是命名略有不同:

  • key:账户的地址或公钥。类型为 address
  • lamports:账户的 lamport 余额。类型为 uint64
  • data:账户的数据。类型为 bytes
  • owner:账户的所有者。类型为 address
  • rent_epoch:账户下一次应付租金的 epoch。类型为 uint64
  • is_signer:指定账户是否签署了交易。类型为 bool
  • is_writable:指定账户在此交易中是否可写。类型为 bool
  • executable:指定账户是否为程序。类型为 bool

限制

Solang 与传统 Ethereum 开发的主要不兼容之处包括:

  • msg.sender 在 Solana 上不可用。在账户模型中,一个 Rust 合约可以访问多个数据账户,那么应将其中哪个视为调用者?在许多情况下,我们无法将某个账户单独认定为调用者。此外,运行时也没有获取调用者账户的机制
  • 没有 ecrecover() 函数,但存在用于检查 ed25519 签名的 signatureVerify() 函数。
  • try-catch 语句无法使用。如果任何外部调用或合约创建失败,运行时将停止执行并回滚整个交易。
  • 错误定义以及附带错误消息的回滚功能尚不可用
  • 通过函数调用转移原生价值不可用。
  • 许多 Yul 内置函数不可用。Solang 支持大多数内置函数,但尚未实现内存和链操作。
  • 目前不支持 ERC-20 格式。SPL Token 根据 Token Program 定义。Token Program 是 Solana 创建、铸造、转移和销毁代币的原生方式。应将 spl_token.sol 文件复制到源代码树中,并在需要使用 SplToken 库的位置导入

此外,SVM 的寄存器宽度为 64 位,因此 64 位整数(即 uint64 和 int64)优于 256 位整数。使用宽度超过 64 位的类型(如 uint256 或 int256)执行操作时,会拆分成多个操作。这会降低速度并消耗更多计算单元。

对于地址,地址字面量必须使用 address"36VtvSbE6jVGGQytYWSaDPG7uZphaxEjpJHUUpuUbq4D" 语法指定。不支持 Ethereum 的十六进制语法(例如 0xE0f5206BBD039e7b0592d8918820024e2a7437b9)。Solana 上的所有余额和值均为 64 位。这意味着地址的内置函数(即 .balance()、.transfer() 和 .send())使用 64 位整数。

Solang 为 EVM 开发者进入 Solana 生态提供了一条令人期待的路径,但它也有一些需要仔细考量的限制。转向 Solana 基于账户的模型并不只是语法变化,而是智能合约逻辑的根本转变。Solang 不支持某些函数、编码模式和 EVM 特有功能。开发者不能简单地将 Ethereum 智能合约移植到 Solang,并指望它无需大幅调整就能运行。缺少完善的错误定义和附带错误消息的回滚等功能,可能会带来严重问题。不过,必须认识到 Solang 是一个持续演进的工具,每次更新都旨在改善 Solidity 开发者在 Solana 上的体验。Solang 还提供了多项显著优势,进一步优化这种体验。

优势

尽管存在这些限制,Solang 仍提供了许多内置函数和设计选择来改善开发者体验。例如,使用 Solang 开发的合约可以与 Anchor 程序交互。你可以根据 Anchor 程序的 IDL 生成 Solidity 接口来实现这一点。IDL 是“接口描述语言”的缩写。本质上,它是一个包含程序所有规范的 JSON 文件,其中包括与 Anchor 程序交互所需的全部信息。使用 Anchor 框架开发程序时,Anchor 会自动生成 IDL。这与 Ethereum 上的 ABI 非常相似。要从 IDL 生成 Solidity 接口,请使用以下命令:solang idl [-output directory] [IDL file]。现在,可以使用 import “...”; 语法导入该文件。

Solang 提供了 Solana 库,其中包含一系列用于让 Solidity 合约与 Solana 特有指令交互的库。Solang 提供 SPL Token 库,用于铸造、销毁和转移代币。你可以将其视为 ERC-20 和 ERC-721 的对应实现。Solang 还提供系统指令库,让开发者能够与 Solana 的 System Program 交互。

Solang 还包含一些可通过 solana 导入的内置功能,其中包括 AccountMeta 和 AccountInfo 结构体。AccountMeta 结构体用于指定在外部调用(即 CPI)中应将哪些账户传递给被调用方。AccountMeta 结构体具有以下结构:

  • pubkey:账户的地址或公钥。类型为 address
  • is_writable:指定被调用方是否可以写入此账户。类型为 bool
  • is_signer:指定被调用方是否可以假定此账户已签署交易。类型为 bool

如果外部调用中省略 accounts 参数,Solang 编译器会自动生成 AccountMeta 数组。只有将函数声明为 external 时才会生效。否则,必须按照 IDL 中指定的账户顺序手动创建 AccountMeta 数组。如果某个调用不需要账户,请传入空向量:{accounts: []}。Solang 文档提供了一个可靠的示例,说明如何构建 AccountMetas 数组:

代码
function build_this() external {
	// When calling a constructor from an external function, the data account for the contract
	// 'BeingBuilt' should be passed as the 'BeingBuilt_dataAccount' in the client code.
	BeingBuilt.new("my_seed");
}

function build_that(address data_account, address payer_account) public {
	AccountMeta[3] metas = [
		AccountMeta({
			pubkey: data_account,
      is_signer: true,
      is_writable: true
		}),
    AccountMeta({
    	pubkey: payer_account,
      is_signer: true,
      is_writable: true
    }),
    AccountMeta({
      pubkey: address"11111111111111111111111111111111",
      is_writable: false,
      is_signer: false
    })
  ];
  BeingBuilt.new{accounts: metas}("my_seed");

	// No accounts are needed in this call, so we pass an empty vector.
	BeingBuilt.say_this{accounts: []}("It's summertime!");
}

Solang 还为以下操作提供内置函数:

Solang 提供功能丰富的编译器工具集,帮助 EVM 开发者更轻松地迁移,从而弥合 Solana 与 Ethereum 之间的鸿沟。尽管存在限制,Solang 仍提供多项改善开发者体验的功能,包括与 Anchor 程序交互、访问 Solana 特有库以及使用内置函数。IDL 和代币标准等熟悉概念的集成进一步降低了学习门槛。无需担心 gas 或针对 gas 进行优化也是一个受欢迎的改进。Solang 代表着 Solana 与 Ethereum 实现互操作性的重要一步。对于希望进入 Solana 高性能世界的 EVM 开发者来说,Solang 是一个可选工具。

Neon EVM

对于希望在 Solana 上构建应用的 Solidity 开发者来说,Solang 并非唯一选择。Neon EVM 自称是全球首个可并行化的 EVM。它是 Solana 上完全兼容 Ethereum 的环境。对于希望以开发者友好的方式在 Solana 上扩展 Ethereum dApp 的任何人来说,这都是一种协同解决方案。开发者可以使用自己熟悉的语言和工具部署 dApp,而无需重新配置智能合约。

架构

Neon EVM 由三个主要组件构成:Neon EVM 程序、Neon Proxy 和 Neon DAO。

Neon EVM 是一个 Solana 程序,它接受类似 Ethereum 的交易,并按照 EVM 规则在 Solana 上处理这些交易。发送到 Neon EVM 的此类交易称为 Neon Transactions。按照 Ethereum JSON-RPC API,它们属于 JSON RPC 方法的一个子集。

Neon Proxy 让 Ethereum 开发者只需进行少量更改,即可将 dApp 移植到 Neon。它将 EVM 交易封装到 Solana 交易中,为 Neon Operator 提供容器化解决方案。这些运营者运行 Neon Proxy 服务器,接受以 NEON 支付的款项,并在 Solana 生态中使用 SOL 付款。NEON 兼具实用代币和治理代币的功能:Neon Operator 收取 NEON,用于支付执行交易所需的 gas 费用;NEON 持有者还可以参与 Neon DAO。

Neon DAO 是一种社区驱动的治理模型,旨在让 NEON 代币持有者能够参与 Neon EVM 的决策流程。它由用户、运营者、贡献者、核心开发者和应用开发者组成,这些参与者共同讨论治理规则和协议演进。DAO 通过多个专注于生态、开发和安全的去中心化议会运行。每个议会负责推动各自领域的协作决策和提案审查。专注于生态的议会负责监督生态的可持续增长,并管理用于资助和倡议的资金。专注于开发的议会负责 Neon 程序的技术升级和紧急干预。专注于安全的议会负责保护 Neon 财库和 Neon 程序免受潜在威胁。

EVM 兼容性

Neon EVM 通过以下方式支持在 Solana 上进行 EVM 交互:

与 Neon EVM 交互的方式与其他 EVM 类似。开发者可以向 Neon Proxy 调用熟悉的 RPC API 方法,获得顺畅的开发体验。主要功能包括:

  • 兼容 Solidity 和 Vyper 智能合约,并支持 Metamask、Foundry 和 Remix 等标准 Ethereum 开发工具
  • 原样支持大多数 Ethereum Opcode
  • 接受类型 0/传统 Ethereum 交易请求。目前不支持 EIP-1559 交易

当然,要让 Neon EVM 在 Solana 上正确运行,仍需做出一些适配。主要差异包括:

  • Neon EVM 支持 evm.code 上定义的所有预编译合约。但包含以下调用的 Solidity 合约将不会执行:bigModExp、bn256Add、bn256ScalarMult 和 bn256Pairing。要在未来支持这些合约,Neon EVM 需要实现 Solana 系统调用
  • 尽管大多数 opcode 均按原样支持,但不支持 COINBASE、PREVRANDAO (FKA DIFFICULTY)、GASLIMIT、BASEFEE 和 GAS。这些称为变体 opcode,已针对 Neon EVM 进行适配。
  • gas 消耗和费用计算与 Ethereum 不同。由于 Solana 充当结算层,成本通常更低
  • 由于 gas 计算方式不同,Solidity 的 transfer() 和 send() 方法在 Neon EVM 中不具备重入安全性
  • Solana 的账户模型会影响智能合约存储,可执行账户与不可执行账户的存储功能和访问权限有所不同
  • Neon EVM 使用版本化交易,将单笔交易可使用的账户数量上限限制为 64 个
  • Neon EVM 使用 Solana 的 Berkeley Packet Filter(BPF),堆内存上限为 256 KB。这限制了合约调用的大小,因此需要采用优化策略来有效管理内存使用
  • block.number 和 block.timestamp 等基于时间的函数行为有所不同。强烈建议开发者在 Neon EVM 上开发时不要使用这些函数

虽然 Neon EVM 提供了熟悉且兼容的环境,但 EVM 开发者必须了解并适应这些主要差异,才能成功迁移并在 Solana 上构建应用。

连接 Neon RPC

开发者可以使用 Chainlist 轻松连接 Neon RPC。在这里,开发者可以连接 Neon EVM Mainnet 或 Devnet。在 Mainnet 或 Devnet 弹窗中点击 Connect Wallet,然后在钱包弹出时点击 Approve。

用户向 Neon EVM 发送交易前,应选择最合适的运营者。展开卡片详情,查看每个网络可用的 RPC 端点:

如果你选择的运营者不是连接钱包时提供的默认运营者,则需要手动连接。Neon EVM 文档提供了使用 Foundry、Hardhat、Remix 和 Truffle 连接 Proxy 的完整指南。我们将在部署部分介绍如何使用 Foundry 连接。

连接后,开发者可以使用 Neon Faucet 获取 NEON 或其他 ERC-20 测试代币。开发者也可以通过 request_neon 端点以编程方式请求代币:

代码
curl -i -X POST \
	-d '{"wallet": "Your wallet", "amount": 1}' \
	'http://localhost:3333/request_neon'

此命令发送 POST 请求,将代币发送到指定的钱包地址。

交易生命周期和 Gas 费用

通过 Neon EVM 在 Solana 上执行来自 Ethereum dApp 的交易主要分为三个步骤:

  • 用户发起一笔已签名、发送至 Neon RPC 端点的类 Ethereum 交易
  • 交易通过 Ethereum API 传递给 Neon Proxy。Proxy 估算执行交易所需的 gas,将类 Ethereum 交易封装成 Solana 交易并发起广播,然后将封装后的交易发送到 Neon EVM。这样会生成一份 Solana 交易收据及相应的 Neon EVM 交易收据。Neon 智能合约解封交易、验证用户签名,并从 Solana 存储中加载 EVM 状态。交易在 Solana 的 BPF 内执行
  • Solana 和 Neon EVM 更新各自状态,完成交易请求

这就是 Neon EVM 从发起到执行的完整交易生命周期。开发者可以访问 NeonScan 查看最新交易和区块,并查询账户、代币、区块或交易哈希。

开发者也可以发送免 gas 交易。该功能旨在支持 NEON 代币不足、无法支付初始交易费用的用户。开发者可以联系 info@neonevm.org,获取一组初始免 gas 交易。这些交易仍由开发者选择的 Proxy Operator 处理,但交易成本由 Neon 承担。通常,每个新的 Neon 账户至少可获得三笔免 gas 交易。

免 gas 交易的流程如下:

  • 最终用户通过 dApp 发起交易
  • dApp 向所选 Proxy Operator 请求当前 gas 价格。如果符合条件,Proxy Operator 会为 Neon 账户标记指定数量的免 gas 交易
  • 对于这些交易,dApp 将显示 gas 成本为零
  • 最终用户签署无需 gas 费的交易
  • Proxy Operator 执行交易,SOL gas 费用由 Neon Foundation 支付

Neon EVM 文档提供了以下摘录,用于演示如何请求免 gas 交易:

代码
try {
	// Get gasless transaction if user account is eligible
  const rawGasPrice = await axios.post(rpcApiUrl, {
  	method: 'neon_gasPrice',
    params: [{ from: address }],
    jsonrpc: "2.0",
    id: new Date().getTime()
   })

   tx.gasPrice = rawGasPrice.data?.result;
	} catch (e) {
  	//Else, get standard GAS price
   	setError('Can\'t retrieve gas price for transaction')

    const rawGasPrice = await web3.eth.getGasPrice();

    tx.gasPrice = web3.utils.toHex(rawGasPrice);
} finally {
    setTx(tx)
}

NeonPass

NeonPass 是用于在 Solana 与 Neon EVM 之间转移代币的工具。它支持在 Solana 的关联代币账户与 Neon EVM 的 ERC-20 代币账户之间无缝转移资产。NeonPass 使用 Neon EVM 的接口合约和专用账户存储。Solana Program Library(SPL)代币会在 Neon EVM 的工厂合约中封装到 ERC-20 接口内。这使 SPL 能够存储在兼容 Solidity dApp 的 ERC-20 代币账户中。NeonPass 支持在 Solana 与 Neon EVM 账户之间双向转移代币。与锁定资产并铸造新资产的传统跨链桥不同,它直接在两种账户类型之间移动代币,从而实现 Solana 和 Neon EVM 代币的无缝迁移。

部署

开发者可以使用 Hardhat、Foundry、Truffle 和 Remix 部署到 Neon EVM。由于使用 Solang 最简单的方式是通过 Anchor,因此所有测试都使用 TypeScript 完成。不过,对于所有 Solidity 忠实用户来说,我们终于可以通过 Foundry 在 Solana 上测试和部署 Solidity dApp。

首先,确保你有一个兼容 EVM 的钱包,并已连接到 Neon EVM Devnet。然后,克隆 Neon 的 Foundry 示例项目并进入该项目:

代码
git clone https://github.com/neonlabsorg/neon-tutorials
cd neon-tutorials/foundry

然后安装 Foundryup,即 Foundry 工具链安装程序,并运行 foundryup 安装最新的(nightly)预编译二进制文件(即 force、cast、anvil 和 chisel):

代码
curl -L https://foundry.paradigm.xyz | bash
foundryup

安装所需的库:

代码
forge install foundry-rs/forge-std --no-commit
forge install openzeppelin/openzeppelin-contracts --no-commit

现在,获取钱包账户的私钥。以 Metamask 为例,你可以点击汉堡菜单并进入 Account Details > Show Private Key 来查看私钥。系统会提示你输入密码。点击 Confirm 即可访问账户私钥。请勿与任何人分享此密钥,并采取必要措施保护它。

然后,创建包含以下变量的 .env 文件:

代码
RPC_URL_DEVNET=https://devnet.neonevm.org
CHAIN_ID_DEVNET=245022926
RPC_URL_MAINNET=https://neon-proxy-mainnet.solana.p2p.org
CHAIN_ID_MAINNET=245022934
PRIVATE_KEY=
VERIFIER_URL_BLOCKSCOUT=https://neon-devnet.blockscout.com/api

将 <YOUR_PRIVATE_KEY> 替换为你的私钥,然后运行 source .env。

要编译项目合约,请进入 src 目录并运行 forge build。控制台应返回编译器运行成功的消息。也可以使用 forge test 命令测试合约。要部署项目合约,请运行以下命令:

代码
forge create --rpc-url $RPC_URL_DEVNET --private-key $PRIVATE_KEY src/TestERC20/TestERC20.sol:TestERC20 --constructor-args "Test ERC20 Token" "TERC20" --legacy

你应会看到类似以下内容的输出:

代码
[⠰] Compiling...
No files changed, compilation skipped
Deployer: 0x4455E84Eaa56a01676365D4f86348B311969a4f4
Deployed to: 0x5537599aa2F97Dd60a66342522a465A7f2e40Ff9
Transaction hash: 0x6de9dab8a526cbac33008056d185b93dff725605efb791bf116b6bece4f0c486

要验证合约,请运行以下命令:

代码
forge verify-contract --chain-id $CHAIN_ID_DEVNET  src/TestERC20/TestERC20.sol:TestERC20 --verifier-url $VERIFIER_URL_BLOCKSCOUT --verifier blockscout

将 <contract_address> 替换为智能合约的地址。你应该会收到 OK 响应,其中包含一个指向 Neon Devnet 浏览器 BlockScout 上该合约地址的 URL。你也可以配置 .env 文件,使用同样支持 devnet 的 NeonScan 代替 BlockScout。

优势与限制

希望获得类似 Ethereum 开发体验的开发者应选择 Neon EVM。它是一个兼容 EVM 的环境,可发送类 Ethereum 交易并使用熟悉的工具。NeonPass、对大多数 EVM opcode 的支持以及免 gas 交易等功能,让迁移到 Solana 变得轻松许多。但 Neon EVM 并不完美。它也有一些限制,例如需要调整智能合约逻辑以适配 Solana 基础设施、必须在 Solana Berkeley Packet Filter(BFP)和账户模型的约束下运行,以及对某些 opcode 和预编译合约存在限制。要从兼容 EVM 的环境成功部署到 Solana,理解并妥善处理这些细微差异至关重要。

过往迁移案例

大型 Ethereum 协议要部署到非 EVM 链上,必须完成多项复杂任务。具体来说,需要招募非 Solidity 工程师从头重建代码库、寻找值得信赖的审计合作伙伴、重新审计新代码库,并调整治理合约以执行 DAO 决策。这些工作可能耗资数百万美元,还需要数月的专项投入。对大多数项目而言,这并不可行。然而,此前已有成功先例。

Helium 是一个 LoRaWAN 网络,旨在创建去中心化无线基础设施,为物联网 (IoT) 设备提供支持。它通过热点设备实现这一目标。这些低功耗小型设备类似微型信号塔,可以远距离连接其他热点。Helium 最初运行在自己的 Layer 1 (L1) 区块链上,随后其开发团队通过 HIP 70 提议迁移到 Solana。迁移后,Helium 可以在保持高安全性和低使用成本的同时,实现更高的可用性、更强的可组合性和更快的用户体验。社区以压倒性多数投票支持该提案,Helium 于 2023 年 4 月迁移到 Solana。Helium 首席运营官 Scott Sigel 称这次迁移平淡无奇——网络和 Helium 基础设施均未出现任何问题。这正是工程师梦寐以求的结果。该团队还在文档的一系列指南中详细介绍了整个迁移流程。Helium 的迁移非常成功,为社区带来了巨大收益。

这次迁移并非个例。全球首个去中心化 GPU 渲染平台 The Render Network 于 2023 年 11 月成功将其核心基础设施从 Ethereum 升级到 Solana。社区投票通过了迁移至 Solana 的 RNP-002 提案。Render 创始人 Jules Urbach 称此次迁移是一个分水岭。他表示:“Solana 拥有惊人的交易速度、低廉的成本,并致力于打造 Web 级架构。随着我们继续构建可扩展的去中心化元宇宙基础设施,它是 Render Network 的理想选择。”

Render 的迁移并未对用户造成过多负面影响。用户可以使用 Render 的升级助手将资金从 Ethereum 跨链转移到 Solana。用户只需连接 Ethereum 钱包,输入希望迁移的 RNDR 数量,然后等待代币发送到指定的 Solana 钱包。 

Maker 是典型的 Ethereum 项目。其目标是通过 Maker Platform 释放去中心化金融的潜力。这个包容性平台旨在增强经济自主权,让所有人都能平等地进入全球金融市场。该平台由负责管理 Maker 项目的 MakerDAO,以及支持 DAI 的 Maker 协议组成。DAI 是“全球首个无偏见货币,也是领先的去中心化稳定币”。Maker 创始人 Rune Christensen 曾发帖讨论如何使用 Solana 代码库的分叉来开发 Maker appchain: 

迁移本身十分复杂。尽管将项目的全部基础设施从 Ethereum 迁移到 Solana 会产生资金、声誉和时间成本,仍有项目选择 Solana。借助 Solang 和 Neon EVM 等工具,迁移不必如此复杂。它可以像 Helium 的迁移一样平淡无奇,也可以像 Render Network 一样,几乎不对用户资金造成任何不利影响。Solana 是市场上性能最高的区块链,专为 Web 级规模而打造。这里正是构建项目的理想之地,越来越多的项目也开始意识到这一点。

结论

Solidity 是智能合约开发领域的通用语言。自诞生以来,EVM 一直是主导性的智能合约环境,但它也存在不足。Web 级、消费者级应用需要高吞吐量、低延迟的网络来支持其运行。Ethereum 采用单线程、Gas 费用波动较大的环境,无法支持 Helium 这类高吞吐量的去中心化物理基础设施网络 (DePIN) 项目。 

面对这些挑战,Solana 成为了强有力的替代方案。要充分发挥 Solana 的真正优势,最好的方式就是直接在 Solana 上构建。随着近期生态的发展,Solang 和 Neon EVM 等工具让感兴趣的 EVM 开发者能够使用熟悉的工具和语言完成迁移。本文全面介绍了 Solana 的架构、它与 Ethereum 的对比,并探讨了 EVM 开发者如何开始在 Solana 上进行开发。Solana 是市场上性能最高的区块链。它的发展势头、关注度以及信誉良好的消费者级应用都在不断增长。既然现在就能享受快速、可扩展区块链的优势,又何必等待 Ethereum 实现扩容?

如果你读到了这里,感谢你,匿名朋友!请务必在下方填写邮箱地址,这样就不会错过 Solana 的任何最新动态。准备好深入探索了吗?立即加入我们的 Discord,在性能最高的区块链上开始构建未来。

更多资源

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片