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

中级3/19/2024, 7:05:09 AM
Starknet支持原生级别的AA账户抽象,允许高度定制的交易处理方案,并采取多项反制措施保障安全。这些特性为Starknet创造了必要的条件,支持存储分层、检测垃圾合约等功能,尽管某些功能尚未落地,但为AA生态的探索提供了重要基础。

摘要:·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节点设备上直接运行。

引入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字节码部署到以太坊链上。


(图片来源:not-satoshi.com)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,就可以完成业务逻辑升级,不需要把资产状态迁移到新去处,这种合约升级形式比以太坊的更彻底、更原生。

而以太坊合约要变更业务逻辑,往往就要把业务逻辑“外包”给代理合约,通过变更依赖的代理合约,来实现主合约业务逻辑的变更,但这种方式不够简洁,也“不原生”。

(图片来源:wtf Academy)

在某些场景下,如果旧的以太坊合约被整个弃用,里面的资产状态就无法直接迁移到新去处,非常麻烦;而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那么灵便。

(图片来源:ArgentWallet)但目前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节点客户端中直接实现了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报错信息。For Example,假如存在以下代码段

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

StarknetAA合约概述

考虑到本文有部分读者可能存在编程背景,所以此处简单展示了一下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生态做出了重要的探索。

声明:

  1. 本文转载自[极客 Web3],著作权归属原作者[Shew & Faust],如对转载有异议,请联系Gate Learn团队,团队会根据相关流程尽速处理。
  2. 免责声明:本文所表达的观点和意见仅代表作者个人观点,不构成任何投资建议。
  3. 文章其他语言版本由Gate Learn团队翻译, 在未提及Gate.io的情况下不得复制、传播或抄袭经翻译文章。

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

中级3/19/2024, 7:05:09 AM
Starknet支持原生级别的AA账户抽象,允许高度定制的交易处理方案,并采取多项反制措施保障安全。这些特性为Starknet创造了必要的条件,支持存储分层、检测垃圾合约等功能,尽管某些功能尚未落地,但为AA生态的探索提供了重要基础。

摘要:·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节点设备上直接运行。

引入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字节码部署到以太坊链上。


(图片来源:not-satoshi.com)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,就可以完成业务逻辑升级,不需要把资产状态迁移到新去处,这种合约升级形式比以太坊的更彻底、更原生。

而以太坊合约要变更业务逻辑,往往就要把业务逻辑“外包”给代理合约,通过变更依赖的代理合约,来实现主合约业务逻辑的变更,但这种方式不够简洁,也“不原生”。

(图片来源:wtf Academy)

在某些场景下,如果旧的以太坊合约被整个弃用,里面的资产状态就无法直接迁移到新去处,非常麻烦;而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那么灵便。

(图片来源:ArgentWallet)但目前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节点客户端中直接实现了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报错信息。For Example,假如存在以下代码段

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

StarknetAA合约概述

考虑到本文有部分读者可能存在编程背景,所以此处简单展示了一下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生态做出了重要的探索。

声明:

  1. 本文转载自[极客 Web3],著作权归属原作者[Shew & Faust],如对转载有异议,请联系Gate Learn团队,团队会根据相关流程尽速处理。
  2. 免责声明:本文所表达的观点和意见仅代表作者个人观点,不构成任何投资建议。
  3. 文章其他语言版本由Gate Learn团队翻译, 在未提及Gate.io的情况下不得复制、传播或抄袭经翻译文章。
即刻开始交易
注册并交易即可获得
$100
和价值
$5500
理财体验金奖励!
It seems that you are attempting to access our services from a Restricted Location where Gate is unable to provide services. We apologize for any inconvenience this may cause. Currently, the Restricted Locations include but not limited to: the United States of America, Canada, Cambodia, Thailand, Cuba, Iran, North Korea and so on. For more information regarding the Restricted Locations, please refer to the User Agreement. Should you have any other questions, please contact our Customer Support Team.