Layer 1 解读 Starknet 智能合约模型与原生 AA:特立独行的技术巨匠

geek-web3 · 2024年03月10日 · 17 次阅读

摘要:·Starknet 最主要的几大技术特性,包括利于 ZK 证明生成的 Cairo 语言、原生级别的 AA、业务逻辑与状态存储相独立的智能合约模型。

  • Cairo 是一种通用的 ZK 语言,既可以在 Starknet 上实现智能合约,也可以用于开发偏传统的应用,其编译流程中引入 Sierra 作为中间语言,使得 Cairo 可以频繁迭代,但又不必变更最底层的字节码,只需要把变化传导至中间语言身上;在 Cairo 的标准库内,还纳入了账户抽象所需要的许多基本数据结构。
  • Starknet 智能合约将业务逻辑与状态数据分开来存储,不同于 EVM 链,Cairo 合约部署包含“编译、声明、部署”三阶段,业务逻辑被声明在 Contract class 中,包含状态数据的 Contract 实例可以与 class 建立关联,并调用后者所包含的代码;

  • Starknet 的上述智能合约模型利于代码复用、合约状态复用、存储分层、检测垃圾合约,也利于存储租赁制和交易并行化的实现。虽然后两者目前暂未落地,但 Cairo 智能合约的架构,还是为其创造了“必要条件”。
  • Starknet 链上只有智能合约账户,没有 EOA 账户,从一开始就支持原生级别的 AA 账户抽象。其 AA 方案一定程度吸收了 ERC-4337 的思路,允许用户选择高度定制化的交易处理方案。为了防止潜在的攻击场景,Starknet 做出了诸多反制措施,为 AA 生态做出了重要的探索。

前言

继 Starknet 发行代币之后,STRK 逐渐成为以太坊观察者眼中不可或缺的要素之一。这个向来以“特立独行”“不重视用户体验”而闻名的以太坊 Layer2 明星,就像一个与世无争的隐士,在 EVM 兼容大行其道的 Layer2 生态里默默的开辟自己的一亩三分地。

由于太过忽视用户,甚至公开在 Discord 开设“电子乞丐”频道,Starknet 一度遭到撸毛党的抨击,在遭喷“不近人情”的同时,技术上的深厚造诣瞬间变得“一文不值”,似乎只有 UX 和造富效应才是一切。《金阁寺》中那句“不被人理解成了我唯一的自豪”,简直就是 Starknet 的自我写照。

但抛开这些江湖琐事,单纯从代码极客们的“技术品味”出发,作为 ZK Rollup 先驱之一的 Starknet 和 StarkEx,几乎就是 Cairo 爱好者眼中的瑰宝,在某些全链游戏开发者心中,Starknet 和 Cairo 简直就是 web3 的一切,无论是 Solidity 还是 Move 都无法与之相提并论。现如今横亘在“技术极客”和“用户”之间的最大代沟,其实更多归因于人们对 Starknet 的认知欠缺。

抱着对区块链技术的兴趣与探索欲,以及对 Starknet 的价值发现,本文作者从 Starknet 的智能合约模型与原生 AA 出发,为大家简单梳理其技术方案与机制设计,在为更多人展示 Starknet 技术特性的同时,也希望让人们了解这个“不被人所理解的独行侠”。

Cairo 语言极简科普

下文中我们将重点讨论 Starknet 的智能合约模型与原生账户抽象,说明 Starknet 是如何实现原生 AA 的。读完此文,大家也可以理解为什么 Starknet 中不同钱包的助记词不能混用。

但在介绍原生账户抽象前,让我们先了解下 Starknet 独创的 Cairo 语言。在 Cairo 的发展历程中,出现了名为 Cairo0 的早期版本,以及后来的的现代版。Cairo 的现代版本整体语法类似于 Rust,实际上是一门通用的 ZK 语言,除了可以在 Starknet 上编写智能合约,也可以用于通用应用的开发。

比如我们可以用 Cairo 语言开发 ZK 身份验证系统,这段程序可以在自己搭建的服务器上运行,不必依赖于 StarkNet 网络。可以说,任何需要可验证计算属性的程序都可以用 Cairo 语言来实现。而 Cairo 可能是目前最利于生成 ZK 证明的编程语言。

从编译流程来看,Cairo 使用了基于中间语言的编译方法,如下图所示。图中的 Sierra 是 Cairo 语言编译过程中的一道中间形态 (IR),而 Sierra 会再被编译为更底层的二进制代码形式,名为 CASM,在 Starknet 节点设备上直接运行。

Cairo 编译方法

引入 Sierra 作为中间形态,便于 Cairo 语言增加新特性,许多时候只要在 Sierra 这道中间语言上做手脚,不必直接变更底层的 CASM 代码,这就省去了很多麻烦事,Starknet 的节点客户端就不必频繁更新。这样就可以在不变更 StarkNet 底层逻辑的情况下,实现 Cairo 语言的频繁迭代。而在 Cairo 的标准库内,还纳入了账户抽象所需要的许多基本数据结构。

Cairo 的其他创新,包括一种被称为 Cairo Native 的理论方案,该方案计划把 Cairo 编译为能适配不同硬件设备的底层机器代码,Starknet 节点在运行智能合约时,将不必依赖于 CairoVM 虚拟机,这样可以大幅度提升代码执行速度【目前还处于理论阶段,未落地】。

Starknet 智能合约模型:代码逻辑与状态存储的剥离

与 EVM 兼容链不同,Starknet 在智能合约系统的设计上,有着突破性的创新,这些创新很大程度是为原生 AA 以及未来上线的并行交易功能准备的。在这里,我们要知道,以太坊等传统公链上,智能合约的部署往往遵循“编译后部署”的方式,以 ETH 智能合约举例:

  1. 开发者在本地编写好智能合约后,通过编辑器将 Solidity 程序编译为 EVM 的字节码,这样就可以被 EVM 直接理解并处理;
  2. 开发者发起一笔部署智能合约的交易请求,把编译好的 EVM 字节码部署到以太坊链上。

ETH智能合约

Starknet 的智能合约虽然也遵循“先编译后部署”的思路,智能合约以 CairoVM 支持的 CASM 字节码形式部署在链上,但在智能合约的调用方式与状态存储模式上,Starknet 与 EVM 兼容链有着巨大差异。

准确的说,以太坊智能合约=业务逻辑 + 状态信息,比如 USDT 的合约中不光实现了 Transfer、Approval 等常用的函数功能,还存放着所有 USDT 持有者的资产状态,代码和状态被耦合在了一起,这带来了诸多麻烦,首先不利于 DAPP 合约升级与状态迁移,也不利于交易的并行处理,是一种沉重的技术包袱

以太坊智能合约交互流程

对此,Starknet 对状态的存储方式进行了改良,在其智能合约实现方案中,DAPP 的业务逻辑与资产状态完全解耦,分别存放在不同地方,这样做的好处很明显,首先可以让系统更快速的分辨出,是否存在重复或多余的代码部署。这里的原理是这样:

以太坊的智能合约=业务逻辑 + 状态数据,假如有几个合约的业务逻辑部分完全一致,但状态数据不同,则这几个合约的 hash 也不同,此时系统难以分辨出这些合约是否冗余,是否有“垃圾合约”存在。

在 Starknet 的方案中,代码部分和状态数据直接分开,系统根据代码部分的 hash,更容易分辨出是否有相同的代码被多次部署,因为他们的 hash 是相同的。这样便于制止重复的代码部署行为,节约 Starknet 节点的存储空间。

在 Starknet 的智能合约系统中,合约的部署与使用,分为“编译、声明、部署”三个阶段。资产发行者如果要部署 Cairo 合约,第一步要在自己的设备本地,把写好的 Cairo 代码,编译为 Sierra 以及底层字节码 CASM 形式。

然后,合约部署者要发布声明“declare”交易,把合约的 CASM 字节码和 Sierra 中间代码部署到链上,名为Contract Class

Starknet 智能合约

之后,如果你要要采用该资产合约里定义的函数功能,可以通过 DAPP 前端发起“deploy"交易,部署一个和 Contract Class 相关联的Contract 实例,这个实例里面会存放资产状态。之后,用户可以调用 Contract Class 里的函数功能,变更 Contract 实例的状态。

其实,但凡了解面向对象编程的人,都应该能很容易的理解 Starknet 这里的 Class 和 Instance 各自代表啥。开发者声明的 Contract Class,只包含智能合约的业务逻辑,是一段谁都可以调用的函数功能,但没有实际的资产状态,也就没有直接实现“资产实体”,只有“灵魂”没有“肉体”

而当用户部署具体的 Contract 实例后,资产就完成了“实体化”。如果你要对资产“实体”的状态进行变更,比如把自己的 token 转移给别人,可以直接调用 Contract Class 里写好的函数功能。上述过程就和传统面向对象编程语言里的“实例化”有些类似(但不完全一致)

智能合约被分离为 Class 和实例后,业务逻辑与状态数据解耦合,为 Starknet 带来了以下特性:

1.利于存储分层和“存储租赁制”的实现

所谓的存储分层,就是开发者可以按照自己的需求,将数据放在自定义的位置,比如 Starknet 链下。StarkNet 准备兼容 Celestia 等 DA 层,DAPP 开发者可以将数据存放在这些第三方 DA 层里。比如一个游戏可以将最重要的资产数据存放在 Starknet 主网上,而将其他数据存储在 Celestia 等链下 DA 层。这种按照安全需求定制化选择 DA 层的方案,被 Starknet 命名为"Volition"

而所谓的存储租赁制,是指每个人应当持续的为自己占用的存储空间付费。你占用的链上空间有多少,理论上就该持续的支付租金。

在以太坊智能合约模型中,合约的所有权不明确,难以分辨出一个 ERC-20 合约应该由部署者还是资产持有者支付“租金”,迟迟没有上线存储租赁功能,只在合约部署时向部署者收取一笔费用,这种存储费用模型并不合理。

而在 Starknet 和 Sui 以及 CKB、Solana 的智能合约模型下,智能合约的所有权划分更明确,便于收取存储资金【目前 Starknet 没有直接上线存储租赁制,但未来会实现】

2.实现真正的代码复用,减少垃圾合约的部署

我们可以声明一个通用的代币合约作为 class 存储到链上,然后所有人都可以调用这个 class 里的函数,来部署属于自己的代币实例。而且合约也可以直接调用 class 内的代码,这就实现了类似于 Solidity 中的 Library 函数库的效果。

同时,Starknet 的这种智能合约模型,有助于分辨“垃圾合约”。前面对此有所解释。在支持代码复用与垃圾合约检测后,Starknet 可以大幅度减少上链的数据量,尽可能减轻节点的存储压力。

3.真正的合约“状态”复用

区块链上的合约升级主要涉及到业务逻辑的变更,在 Starknet 的场景下,智能合约的业务逻辑与资产状态天生就是分离的,合约实例变更了关联的合约类型 class,就可以完成业务逻辑升级,不需要把资产状态迁移到新去处,这种合约升级形式比以太坊的更彻底、更原生。 而以太坊合约要变更业务逻辑,往往就要把业务逻辑“外包”给代理合约,通过变更依赖的代理合约,来实现主合约业务逻辑的变更,但这种方式不够简洁,也“不原生”。

Starknet 智能合约升级

在某些场景下,如果旧的以太坊合约被整个弃用,里面的资产状态就无法直接迁移到新去处,非常麻烦;而 Cairo 合约就不需要把状态迁移走,可以直接“复用”旧的状态。

4.利于交易并行化处理

要尽可能提升不同交易指令的可并行度,必要一环是把不同人的资产状态分散开存储,这在比特币、CKB 和 Sui 身上可见一斑。而上述目标的先决条件,就是把智能合约的业务逻辑和资产状态数据剥离开。虽然 Starknet 还没有针对交易并行进行深度的技术实现,但未来将把并行交易作为一个重要目标。

Starknet 的原生 AA 与账户合约部署

其实,所谓的账户抽象与 AA,是以太坊社区发明出来的独特概念,在许多新公链中,并没有 EOA 账户和智能合约账户的分野,从一开始就避开了以太坊式账户体系的坑。比如在以太坊的设定下,EOA 账户控制者必须在链上有 ETH 才可以发起交易,没有办法直接选用多样性的身份验证方式,要添加一些定制化的支付逻辑也极为麻烦。甚至有人认为,以太坊的这种账户设计简直就是反人类的。

如果我们去观察 Starknet 或 zkSyncEra 等主打“原生 AA”的链,可以观察到明显的不同:首先,Starknet 和 zkSyncEra 统一了账户类型,链上只有智能合约账户,从一开始就没有 EOA 账户这种东西(zkSync Era 会在用户新创建的账户上,默认部署一套合约代码,模拟出以太坊 EOA 账户的特征,这样就便于兼容 Metamask)。

而 Starknet 没有考虑直接兼容 Metamask 等以太坊周边设施,用户在初次使用 Starknet 钱包时,会自动部署专用的合约账户,说白了就是部署前面提到的合约实例,这个合约实例会和钱包项目方事先部署的合约 class 相关联,可以直接调用 class 里面写好的一些功能。

下面我们将谈及一个有意思的话题:在领取 STRK 空投时,很多人发现 Argent 与 Braavos 钱包彼此不能兼容,将 Argent 的助记词导入 Braavos 后,无法导出对应的账户,这其实是因为 Argent 和 Braavos 采用了不同的账户生成计算方式,导致相同助记词生成的账户地址不同。

具体而言,在 Starknet 中,新部署的合约地址可以通过确定性的算法得出,具体使用以下公式:

上述公式中的 pedersen(),是一种易于在 ZK 系统中使用的哈希算法,生成账户的过程,其实就是给 pedersen 函数输入几个特殊参数,产生相应的 hash,这个 hash 就是生成的账户地址。

上面的图片中显示了 Starknet 生成“新的合约地址”时用到的几个参数,deployer_address 代表“合约部署者”的地址,这个参数可以为空,即便你事先没有 Starknet 合约账户,也可以部署新的合约。

salt 为计算合约地址的盐值,简单来说,就是一个随机数,该变量实际上是为了避免合约地址重复引入的。class_hash 就是前面介绍过的,合约实例对应的 class 的哈希值。而 constructor_calldata_hash,代表合约初始化参数的哈希。

基于上述公式,用户可以在合约部署至链上之前,就预先算出生成的合约地址。Starknet 允许用户在事先没有 Starknet 账户的情况下,直接部署合约,流程如下:

  1. 用户先确定自己要部署的合约实例,要关联哪个合约 class,把该 class 的 hash 作为初始化参数之一,并算出 salt,得知自己生成的合约地址;
  2. 用户知道自己将会把合约部署在哪后,先向该地址转入一定量的 ETH,作为合约部署费用。一般来说,这部分 ETH 要通过跨链桥从 L1 跨到 Starknet 网络;
  3. 用户发起合约部署的交易请求。

其实,所有的 Starknet 账户都是通过上述流程部署的,但大部分钱包屏蔽了这里面的细节,用户根本感知不到里面的过程,就好像自己转入 ETH 后合约账户就部署完了。

上述方案带来了一些兼容性问题,因为不同的钱包在生成账户地址时,生成的结果并不一致,只有满足以下条件的钱包才可以混用:

  1. 钱包使用的私钥派生公钥与签名算法相同;
  2. 钱包的 salt 计算流程相同;
  3. 钱包的智能合约 class 在实现细节上没有根本性不同;

在之前谈到的案例中,Argent 与 Braavos 都使用了 ECDSA 签名算法,但双方的 salt 计算方法不同,相同的助记词在两款钱包中生成的账户地址会不一致。

我们再回到账户抽象的话题上。Starknet 和 zkSync Era 把交易处理流程中涉及的一系列流程,如身份验证 (验证数字签名)、Gas 费支付等核心逻辑,全部挪到“链底层”之外去实现。用户可以在自己的账户中,自定义上述逻辑的实现细节

比如你可以在自己的 Starknet 智能合约账户里,部署专用的数字签名验证函数,当 Starknet 节点收到了你发起的交易后,会调用你在链上账户中自定义的一系列交易处理逻辑。这样显然要更灵活

而在以太坊的设计中,身份验证(数字签名)等逻辑是写死在节点客户端代码里的,不能原生支持账户功能的自定义。


(Starknet 架构师指明的原生 AA 方案示意图,交易验证和 gas 费资格验证都被转移到链上合约去处理,链的底层虚拟机可以调用用户自定义或指定的这些函数)

按照 zkSyncEra 和 Starknet 官方人员的说法,这套账户功能模块化的思路,借鉴了 EIP-4337。但不同的是,zkSync 和 Starknet 从一开始就把账户类型合并了,统一了交易类型,并且用统一入口接收处理所有交易,而以太坊因为存在历史包袱,且基金会希望尽可能避免硬分叉等粗暴的迭代方案,所以支持了 EIP-4337 这种“曲线救国”的方案,但这样的效果是,EOA 账户和 4337 方案各自采用独立的交易处理流程,显得别扭而且臃肿,不像原生 AA 那么灵便。

但目前 Starknet 的原生账户抽象还没有达到完全的成熟,从实践进度来看,Starknet 的 AA 账户实现了签名验证算法的自定义,但对于手续费支付的自定义,目前 Starknet 实际上仅支持 ETH 和 STRK 缴纳 gas 费,并且还没有支持第三方代缴 gas。所以 Starknet 在原生 AA 上的进度,可以说是“理论方案基本成熟,实践方案还在推进”

由于 Starknet 内只有智能合约账户,所以其交易的全流程都考虑了账户智能合约的影响。首先,一笔交易被 Starknet 节点的内存池 (Mempool) 接收后,要进行校验,验证步骤包括:

  1. 交易的数字签名是否正确,此时会调用交易发起者账户中,自定义的验签函数;
  2. 交易发起人的账户余额能否支付得起 gas 费;

这里要注意,使用账户智能合约中自定义的签名验证函数,就意味着存在攻击场景。因为内存池在对新来的交易进行签名验证时,并不收取 gas 费(如果直接收取 gas 费,会带来更严重的攻击场景)。恶意用户可以先在自己的账户合约中自定义超级复杂的验签函数,再发起大量交易,让这些交易被验签时,都去调用自定义的复杂验签函数,这样可以直接耗尽节点的计算资源。

为了避免此情况的发生,StarkNet 对交易进行了以下限制:

  1. 单一用户在单位时间内,可发起的交易笔数有上限;
  2. Starknet 账户合约中自定义的签名验证函数,存在复杂度上的限制,过于复杂的验签函数不会被执行。Starknet 限制了验签函数的 gas 消耗上限,如果验签函数消耗的 gas 量过高,则直接拒绝此交易。同时,也不允许账户合约内的验签函数调用其他合约。

Starknet 交易的流程图如下:

Starknet交易的流程图

值得注意的是,为了进一步加速交易校验流程,Starknet 节点客户端中直接实现了 Braavos 和 Argent 钱包的签名验证算法,节点发现交易生成自这两大主流 Starknet 钱包时,会调用客户端里自带的 Braavos/Argent 签名算法,通过这种类似于缓存的思想,Starknet 可以缩短交易验证时间。

交易数据再通过排序器的验证后(排序器的验证步骤比内存池验证会深入很多),排序器会将来自内存池的交易打包处理,并递交给 ZK 证明生成者。进入此环节的交易即使失败,也会被收取 gas。

但如果读者了解 Starknet 的历史,会发现早期的 Starknet 对执行失败的交易不收取手续费,最常见的交易失败情况是,用户仅有 1ETH 的资金,但是对外转出 10ETH,这种交易显然有逻辑错误,最终必然失败,但在具体执行前谁也不知道结果是啥。

但 StarkNet 在过去不会对这种失败交易收取手续费。这种无成本的错误交易会浪费 Starknet 节点的计算资源,会衍生出 ddos 攻击场景。表面上看,对错误交易收取手续费似乎很好实现,实际上却相当复杂。Starknet 推出新版的 Cairo1 语言,很大程度就是为了解决失败交易的 gas 收取问题。

我们都知道,ZK Proof 是一种有效性证明,而执行失败的交易,其结果是无效的,无法在链上留下输出结果。尝试用有效性证明,来证明某笔指令执行无效,不能产生输出结果,听起来就相当奇怪,实际上也不可行。所以过去的 Starknet 在生成证明时,直接把不能产生输出结果的失败交易都刨除了出去。

Starknet 团队后来采用了更聪明的解决方案,构建了一门新的合约语言 Cairo1,使得“所有交易指令都能产生输出结果并 onchain”。乍一看,所有交易都能产生输出,就意味着从不出现逻辑错误,而大多数时候交易失败,是因为遇到一些 bug,导致指令执行中断了。

让交易永不中断并成功产生输出,很难实现,但实际上有一种很简单的替代方案,就是在交易遇到逻辑错误导致中断时,也让他产生输出结果,只不过这时候会返回一个 False 值,使大家知道这笔交易的执行不顺利。

但要注意,返回 False 值,也就返回了输出结果,也就是说,Cairo1 里面,不管指令有没有遇到逻辑错误,有没有临时中断,都能够产生输出结果并 onchain。这个输出结果可以是正确的,也可以是 False 报错信息

例如假如存在以下代码段

#[external]

fn transfer(to: ContractAddress, amount: u256) -> bool {
  let from = get_caller_address();

  _balances::Write(from, _balances::Read(from) - amount);
  ...
}

此处的 _balances::read(from) - amount 可能因为向下溢出而报错,这个时候就会导致相应的交易指令中断并停止执行,不会在链上留下交易结果;而如果将其改写为以下形式,在交易失败时仍然返回一个输出结果,留存在链上,单纯从观感上来看,这就好像所有的交易都能顺利的在链上留下交易输出,统一收取手续费就显得特别合理

#[external]
fn transfer(to: ContractAddress, amount: u256) -> bool {
  let from = get_caller_address();

  _balances::Write(from, _balances::read(from) - amount);
}

StarknetAA 合约概述

考虑到本文有部分读者可能存在编程背景,所以此处简单展示了一下 Starknet 中的账户抽象合约的接口:

Starknet中的账户抽象合约的接口

  • 上述接口中的 validate_declare,用于用户发起的 declare 交易的验证
  • validate则用于一般交易的验证,主要验证用户的签名是否正确
  • execute则用于交易的执行

我们可以看到 Starknet 合约账户默认支持 multicall 即多重调用。多重调用可以实现一些很有趣的功能,比如在进行某些 DeFi 交互时打包以下三笔交易:

  1. 第一笔交易将代币授权给 DeFi 合约
  2. 第二笔交易触发 DeFi 合约逻辑
  3. 第三笔交易清空对 DeFi 合约的授权

当然,由于多重调用是具有原子性的,所以存在一些更加复杂的用法,比如执行某些套利交易。

总结

  • Starknet 最主要的几大技术特性,包括利于 ZK 证明生成的 Cairo 语言、原生级别的 AA、业务逻辑与状态存储相独立的智能合约模型。
  • Cairo 是一种通用的 ZK 语言,既可以在 Starknet 上实现智能合约,也可以用于开发偏传统的应用,其编译流程中引入 Sierra 作为中间语言,使得 Cairo 可以频繁迭代,但又不必变更最底层的字节码,只需要把变化传导至中间语言身上;在 Cairo 的标准库内,还纳入了账户抽象所需要的许多基本数据结构。
  • Starknet 智能合约将业务逻辑与状态数据分开来存储,不同于 EVM 链,Cairo 合约部署包含“编译、声明、部署”三阶段,业务逻辑被声明在 Contract class 中,包含状态数据的 Contract 实例可以与 class 建立关联,并调用后者包含的代码;
  • Starknet 的上述智能合约模型利于代码复用、合约状态复用、存储分层、检测垃圾合约,也利于存储租赁制和交易并行化的实现。虽然后两者目前暂未落地,但 Cairo 智能合约的架构,还是为其创造了“必要条件”。
  • Starknet 链上只有智能合约账户,没有 EOA 账户,从一开始就支持原生级别的 AA 账户抽象。其 AA 方案一定程度吸收了 ERC-4337 的思路,允许用户选择高度定制化的交易处理方案。为了防止潜在的攻击场景,Starknet 做出了诸多反制措施,为 AA 生态做出了重要的探索。
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号