本文深入探讨了以太坊和Substrate之间的智能合约开发模式的异同,分析了它们在执行限制、存储操作、合约组成和代码更新等方面的关键差异,并为区块链开发者提供了切换技能的背景。此外,还讨论了交易费用和打包调用的差异,以及如何在不同环境中安全地实现复杂逻辑。
在过去几年中,MixBytes 参与了 Polkadot 和 Ethereum 生态系统中的各种项目,担任安全审计师、开发者和测试人员。我们审计和开发了许多 Ethereum/Solidity 合同(相关作品可在我们的 Github 查看),自 Polkadot 的早期版本以来,我们审查和开发了一些 Polkadot/Substrate 代码(请参见我们的 博客),并且仍在与 Substrate 合作(你可以阅读我们关于 Substrate pallets 安全性的 文章)。在本文中,我们旨在分享我们对这两个生态系统的知识。
我们将讨论在表面上完全不同的环境中(Ethereum/EVM 和 Polkadot 的 Substrate/WASM)智能合约开发模式。之所以有趣,是因为这些环境在初看来似乎存在许多不同之处,包括语言和虚拟机。然而,底层存在的许多相似性是由于公共区块链设计和安全限制所造成的严格要求。让我们深入探讨一点。
无论你使用哪种区块链,它们都有相同的限制,要求执行的严格确定性。任何执行状态转换的函数都必须以确定的方式完成,这意味着在相同的区块链状态和输入下,该函数必须返回相同的状态变化。因此,存储变量的布局、算术运算等的内部实现,在任何良好安全的区块链虚拟机中都是相似的。
区块链的另一个严格要求是任何状态转换的复杂性受到限制。Solidity 和 Rust 的图灵完备性仅与语法结构有关。同时,任何用户触发的区块链执行都受计算单位(在 Ethereum 中是“gas”,在 Substrate 中是“weight”)的严格限制。如果没有这些限制,区块链网络就会容易受到拒绝服务(DoS)攻击,用户可以使区块链节点执行“过重”的代码,使整个区块链陷入瘫痪。
目前,智能合约的主要开发模式在无论使用哪种语言和虚拟机的情况下都非常相似。它始终是紧凑的、受限的、用户触发的函数,尽量保持最小的状态,不存储中间数据,主要基于键值操作。
这两种组合各有优点和缺点。其他组合:Rust+EVM(示例:SputnikVM)和 Solidity+WASM(示例:EIP48)也被用于不同的区块链实现。Rust 是一种非常适合在任何区块链上用于智能合约的语言,提供严格的类型、内存管理、非常确定的行为以及许多其他重要的低级功能。另一方面,Solidity 专门为 EVM 设计,并逐渐演变为一种非常严格、高效和安全的语言。
WASM 具有许多可能性,支持众多语言,提供超级灵活的功能,而更简约的 EVM 在许多黑客攻击中已经遭受了这种“超级灵活功能”的影响,并减轻了许多攻击向量。
关于 WASM 的一个重要点是,在区块链中的 WASM 不是可以在浏览器中使用的“完整” WASM;它受到限制,以避免非确定行为。所有使用 WASM 的区块链都使用自己的编译器,添加资源计数器,因此,实际上在智能合约中仅使用 WASM 的一部分,使得区块链 WASM 在某种程度上更接近 EVM。
让我们看看一些示例:
虽然 Solidity 代码看起来简单得多,但任何 Solidity 开发者都知道其背后简单性的隐藏之处在于 balances[to] += amount
- 这是一种映射操作,包括哈希、存储槽计算、溢出检查在内的所有这些过程在 Substrate 中是强制显式的。
在 Rust 中,语法和编译器要求明确行为。例如:在 Rust 中,任何调用结果都应被处理(如果调用返回某个值)。而在 Solidity 中,即使“transfer()”函数返回结果,你仍然可以这样写 transfer(from, to, amount);
(这类未检查转账在 DeFi 中导致了许多安全漏洞)。在 Rust 中,你有义务处理结果,处理可能的错误,从而通过设计减少“未检查调用返回值”(SWC-104)等攻击向量的可能性。
在所呈现的函数中,Substrate 代码的一个重要区别是这一行:
let sender = ensure_signed(_origin)?
它检查交易的签名,在几乎所有地方都使用,而在 Solidity/EVM 中这一点是设计完成的。如果你对 EVM 有可写调用,则交易发送者的签名已被检查,在以太坊中没有未签名的调用(除了“contract-calls-contract”)。在 Substrate 中,你可以创建接受用户调用而无需直接单一签名的函数,允许平行链支持多种签名方案、不同算法,或通过其他方式限制调用。同时,这可能会潜在地打开许多访问控制攻击向量。
EVM 和 Substrate 的 WASM 在区块链中都具有相同角色的事件。此外,两个虚拟机中的底层数据结构、插入算法相似,因为在两个区块链中应用交易的结果是状态数据库中更改的键值集。让我们继续讨论存储。
在 Solidity/EVM 和 Rust/WASM 环境中的存储操作:
任何区块链存储变量(尤其是动态类型:映射、数组)背后的数据结构之间的重要相似之处是使用哈希进行值寻址。映射是区块链中使用最广泛的数据类型,在两个生态系统中实现方式相似。在状态树中构建存储键相似。在 EVM 中,存储键是从“contract address->storage_slot->hashes of keys”构建的,而在 Substrate 中采用的是“module->storagename->hashes of keys”。在两个环境中,Merkle Patricia Tree 被用于在底层存储值,因为它需要高效访问,使用前缀。
另一个重要相似之处是动态数组。在区块链存储中,非固定大小的动态数组与映射没有太大区别,而这是普通非区块链代码的常见现象。同样的确定性限制要求,无论在 EVM 还是 Substrate 中,数组内部都是作为映射构建的,其中数组中的索引是键。因此,无序、易变、可迭代的数组和映射在 EVM 和 Substrate 中并不常见。你可以在 Substrate 中创建一个 IterableStorageMap,但最好避免使用。
一个有趣的区别是在 Substrate 的映射中,你可以选择用于构建指向某个键下值的路径的哈希函数。这可以为了提高性能,代价是牺牲加密安全性,前提是可以接受。相比之下,EVM 仅使用 keccak256() 函数。
Substrate 的运行时存储操作与 EVM 之间的重要区别在于“事务层”。这是通过宏 #[transactional] 和 #[without_transactional] 控制的(文档)。在 EVM 中,如果交易回滚,则所有存储更改也将回滚,而在 Substrate 中,你可以:
现在,#[transactional] 在 Substrate 中对任何外部调用默认开启。
要更详细地解释 Substrate 的存储操作,你可以参考这些 幻灯片,其中涵盖了关于 Substrate 中存储操作的许多要点。此外,可以在 这里 找到关于以太坊数据结构的一篇好文章。
Substrate 和 EVM 之间的一个重大区别是合同/模块的组合及其代码中应用更新的方式。以太坊的合同结构更“分散”,有许多独立的合同被单独部署。而在 Substrate 中,我们有一个单一的整体运行时,由多个模块组成,被组装成单一的字节码,而这些模块只能通过区块链验证者的法定人数进行更改。然而,Substrate 中的模块具有自己的隔离存储,因此从安全角度来看,两种环境中的存储组合看起来有些相似。
在 Substrate 和以太坊中的代码更新在某种意义上既相似又不同。在 Substrate 的情况下,验证者必须就更改达成一致进行投票,而以太坊合同在默认情况下是不可变的。然而,在实践中,大型以太坊项目经常实施合同可升级性,有时这是由利益相关者投票推驱,使情况更接近 Substrate。此外,在 SELFDESTRUCT 从以太坊中删除之前,某些 ETH 地址上的字节码可以通过 CREATE2->CREATE->SELFDESTRUCT->SELFDESTRUCT 的链进行突变(关于此主题的相关文章可以在 这里 找到)。因此,当前在这两个环境中都存在更改智能合约代码的能力。
以太坊中代码/存储的部署和更新要更复杂和“分散”,但仅可能使其项目中断而不影响整个网络。Substrate 验证者几乎可以修复一切,但如果验证者的法定人数工作不良且无法对黑客攻击和漏洞做出反应,那么错误的代价是非常高的,可能会影响整个网络。
在 Substrate 中,开发者似乎被限制无法与外部项目集成,因为他们需要在其网络的运行时中实现所有的副项目。然而,Polkadot 提供了一个关键功能 - 跨链消息传递(XCM) - 它在任何希望使用它的平行链中原生工作。XCM 是平行链之间内置的“消息桥”,允许从一个平行链向另一个平行链发送消息,使用主中继链共识。它使一个平行链中的账户能够与其他平行链中的账户互动,发送资产或实施更复杂的逻辑(我们在第一版 XCM 中进行了测试,效果良好)。当前,XCM 在 Polkadot 中得到了积极使用。
以太坊和 Substrate 中交易费用/权重之间最大的区别在于,在以太坊中,你不能轻易直接控制调用将消耗多少 gas,而在 Substrate 中,被称为“weight”的 gas 的类似物部分由开发者控制。这个概念在 这里 中有更详细的描述(一定要查看 Acala 平行链的 示例,其中不同函数通过常量系数和数据库读/写量实现“加权”)。在 Substrate 中,开发者对交易成本有更多的控制权,网络验证者可以在出现问题时调整用户费用(或发起这种“问题”)。
虽然这看起来是一个显著的区别,但事实上,以太坊中最昂贵的操作涉及 SLOAD/SSTORE,这是状态数据库的读/写,并占据了交易中所用 gas 的大部分。类似地,Substrate 中的权重计算,包括 RocksDbWeight::get().reads() 和 RocksDbWeight::get().writes(),遵循类似的方法。本质上,这两个环境在处理 gas/weight 的方式上是相似的。
现在,让我们看看最终交易费是如何计算的:
inclusion_fee = gas_spent * ( base_fee + priority_fee)
(在 这里 进行了解释)
inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee];
final_fee = inclusion_fee + tip;
(在 这里 进行了解释)
尽管乍一看似乎不同,但在以太坊中的 EIP1559 推出之后,这两个区块链在交易费用方面变得更相似。以太坊中,“length_fee” 已经包含在支出的 gas 中,因此具有两个独立费用组成部分的方案相似:
虽然“gas”和“weight”的计算因为使用两种不同的指令集和虚拟机的关系而大相径庭,但用于“权重”每个指令的“资源计数器”是相似的。所有四种类型的资源(CPU、内存、存储、网络/带宽)都必须考虑,以保持网络安全。
Substrate 的运行时与以太坊智能合约之间还有另一个重要区别。这是一种将多个调用打包成一个原子事务的能力。在以太坊中,这是一种非常常见的模式,有许多情况,用户部署自己的合约,在一次调用中执行多个 DeFi 操作。最好的例子是闪电贷的使用。在以太坊中,用户的合约,例如:
所有这些步骤都在一个原子调用中打包。在 Substrate 中,这种构造不适用。可以使用 utility.batchAll 将多个外部调用按顺序打包为一个原子调用,但这些外部调用(调用)的执行将是隔离的。这去除了以太坊中常见的许多攻击向量,但同时也不允许像闪电贷这样的构造,使市场制造商拥有超级灵活的交易解决方案。使用闪电贷,交易者可以仅从他的余额中开始一个代币(例如 WETH),借用几乎任何代币,仅在单个交易期间,执行交易后再将其资金返还至 WETH,同时还包括“滑点”检查(交易是否有利可图,如果未能反转)。
虽然 Substrate 中没有类似以太坊的原生打包调用设计,但 Substrate 运行时可以执行 EVM,从而实现 几乎 完全复制 EVM 功能。Substrate 有一个公共 pallet_evm,它完全使用 SputnikVM 模拟 EVM,但 EVM 的原生支持尚有很多。一个很好的例子是 Moonbeam.network,允许用户使用与以太坊相同的钱包、客户端软件、地址格式、部署逻辑等与 Moonbeam 互动。该网络具有 XCM 能力,支持与其他平行链交互。
在考虑 EVM 和较低级实现之间的安全性时,提及“ 几乎”这个词是至关重要的。两个级别上应用更改可能会导致数据不同步,此部分代码需要仔细检查。这些问题通常在合同部署、销毁、账户非顺序操作及原生区块链代币/功能与其在 EVM 方面的表现之间的关系中出现。它与“ EVM 在 [你的虚拟机] 中”相关,也与 L1 <-> L2 之间的交互有关。此类问题的良好示例包括:Optimism 无限资金复制 漏洞修复 或 Avalanche 预编译 漏洞。
在以太坊基础网络和 Substrate 基础网络之间,无法简单地判断哪种更好或更差。这两种环境有无数大的和小的差异,在不同的情况下各有优缺点。Substrate 允许非常灵活的区块链设计,进行复杂的计算,并在许多层上对链上逻辑丰富控制,同时不会降低安全性。相反,以太坊允许在顶层构建大规模去中心化项目,基于高安全性的网络,具有许多经过多年验证的现成解决方案和最佳实践。
对于在任一生态系统中经验丰富的智能合约开发者而言,转换到另一个环境并不是什么重大问题,因为许多方法非常相似,即使工具略有不同。主要数据结构的工作原理类似,而且代码的所有重要属性都是相同的。因此,请不要错过并行技术,因为它们可能使你能够构建出伟大的东西。
MixBytes 是一个由区块链审计专家和安全研究人员组成的团队,专门提供 EVM 兼容和 Substrate 基础项目的全面智能合约审计和技术咨询服务。加入我们 X,以获取最新的行业趋势和见解。
- 原文链接: mixbytes.io/blog/substra...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!