作者:WrittenbyChiHaoLu,imTokenLabs本文主要介绍了zkSync这个Layer2解决方案中抽象账户(AA、抽象帐户)的发展及相关内容。重点将放在三个部分:帐户合约:帐户类型,帐户合约的重要入口点和相关重点交易:AA交易的验证方式和执行方式、流程手续费:交易手续费、Paymaster目录导言zkSyncAA合约简要概览zkSync时代的费用模型和Paymaster总结与比较结束语背景熟悉智能合约钱包及其常见特性大致了解以太坊交易的运作方式大致了解EIP-4337的运作模式大致了解ZK(有效性)Rollup的运作模式QuickLookatthezkSync这里为了方便阅读,不需要深入理解zkSync,简要回顾一下zkSync的基本信息。zkSync主要有两个版本,1.0版(zkSyncLite)和2.0版(zkSyncEra)。zkSync1.0版仅支持EOA(外部账户)且不支持智能合约(仅支持代币转账和交换),而zkSync2.0,即zkSyncEra,属于原生AA(抽象账户)(所有账户类型都是合约,没有EOA,即以太坊中的EOA和合约账户的区别),同时兼容EVM(以太坊虚拟机),支持使用Rust、Yul、Vyper、Solidity等开发智能合约。下文提到的zkSync若无特别指称,均指的是zkSync2.0,即zkSyncEra。在zkSyncEra中,还存在多个SystemContract,可以理解为它们将zkSync的一些重要操作系统功能实现在智能合约中。这些SystemContract都是预编译合约,从未被部署(直接在节点中运行),但它们都有一个正式地址。执行AA协议时,zkSync会通过一些SystemContract来进行逻辑运算和判断,例如在验证nonce时,是由NonceHolder来判断,而执行抽象账户机制和收取手续费是由bootloader来判断,下文会逐一介绍它们。RecapAccountAbstraction账户抽象的核心概念可以总结为两个关键点:签名抽象和支付抽象。签名抽象的目标是使各种账户合约能够使用不同的验证方案。这意味着用户不受限于只能使用特定曲线的数字签名算法,而可以选择任何他们喜好的验证机制。而支付抽象旨在为用户提供多种交易支付选项。例如,可以使用ERC-20代币进行支付,而不是使用原生代币,或者可以由第三方赞助交易,甚至是其他更特别的支付模式。zkSync2.0中的账户可以像EOA一样发起交易,但也可以利用其可编程性来实现任意逻辑,如合约账户。这就是我们所谓的帐户抽象(AccountAbstraction),它融合了以太坊中两种账户类型的优势,使AA账户的使用体验更加灵活,从而实现了上述两种目标:签名抽象和支付抽象。zkSyncEra中的AA机制在zkSyncEra中,zkSyncAA的最重要角色是bootloader,它是一个SystemContract,主要用于处理交易以及执行AA机制,对应于EIP-4337的EntryPointContract。bootloader无法被用户调用(只能由Operator触发),也从未被部署(直接在节点上运行),但它具有一个正式地址(可用于收款)。Operator是ZKRollup中的重要角色,是中心化的Off-ChainServer,与可能见过的Sequencer类似,负责从外部触发bootloader等SystemContract。
原生的帐户抽象协议(例如StarkNet、zkSync)基本上都是参考EIP-4337进行设计,zkSync的实现中,用户会将交易发送给Operator,Operator会将交易发送给bootloader,并开始一系列的处理。从区块的角度来看:当bootloader接收到来自Operator的输入时,bootloader会首先为该区块定义一些环境变量(如燃气价格、区块号、区块时间戳等)。然后,bootloader会顺序读取交易列表,首先查询该帐户合约是否同意该交易(即AA机制中的调用validatefunction),然后将它们放入区块中。每笔交易验证通过后,Operator会验证该区块是否足够大,以便发送给验证者(或是否超时)。如果足够大或超时,Operator会关闭该区块,停止向bootloader添加新交易,并完成交易执行。从交易的角度来看,当Operator触发bootloader后,bootloader会顺序处理每笔交易:确认用户帐户合约地址对应的nonce是否合法调用用户帐户合约上的validatefunction进行验证验证通过后,帐户合约会将gasfee汇入bootloader的地址(或通过Paymaster,后文会介绍),bootloader会检查自己是否收到足够的款项。调用用户帐户合约上的executefunction执行交易。以上的前三步对应着EIP-4337的验证循环(VerificationLoop),第四步则对应着EIP-4337的执行循环(ExecutionLoop)。这里主要进行了一个概述性的介绍,每一步的细节和角色将在接下来的详细说明中逐一阐述。zkSync抽象帐户合约快速概览NoncezkSync的账户nonce被记录在一个名为NonceHolder的系统合约中,通过映射(mapping)的方式记住每组(account_address,nonce)对是否被使用,用以判断nonce是否合法。根据前文所述,在Operator触发bootloader后的第一步是检查nonce。因此,在每笔交易开始之前,NonceHolder将用于确认当前使用的这组nonce是否合法(目前仅检查是否已使用)。如果nonce合法,将进入验证阶段(VerificationPhase),此时nonce将被标记为已使用;如果不合法,则交易(验证)将失败。关于zkSync当前nonce的重点:尽管当前用户可以同时向账户发送具有不同nonce的多笔交易进行执行,但由于zkSync不支持并行处理,因此不同nonce的交易仍将按顺序进行处理。理论上,用户可以使用任何256位的非零整数作为nonce,但zkSync仍建议使用incrementNonceIfEquals作为管理nonce的方式,以确保它是按顺序递增的(目前zkSync的AA机制仅确认未使用过的nonce,但官方文件表示未来可能会要求顺序递增)。账户合约在zkSync中的账户合约有以下四个必要的入口点(EntryPoint),分别是:validateTransaction:在验证阶段被调用,以确认此次操作是否经过账户所有者的授权,用户可以在这里定制自己的验证逻辑(例如各种签名算法、多签等)。payForTransaction:当交易手续费由该账户支付(而不是使用paymaster)时,操作员将调用此函数向bootloader地址支付至少tx.gasprice*tx.gaslimit的ETH。prepareForPaymaster:当交易手续费将由Paymaster支付时,操作员将调用此函数以完成与paymaster的交互前准备工作。
zkSync提供的示例是批准Paymaster的ERC-20代币。executeTransaction:在验证阶段成功通过且成功收取手续费后,此函数将用于执行用户希望实现的操作(例如与合约互动、汇款等行为)。关于Paymaster、手续费数量(tx.gasprice*tx.gaslimit)等内容将在后续章节中解释。在zkSync的账户中还有一个非必需的保险函数executeTransactionFromOutside。当无法执行操作时(例如序列生成器没有响应或发现zkSync存在监管风险时),可以使用“逃跑机制”将资金提取到L1。这部分与AA协议没有太大的关系,因此不会在此详细描述,有兴趣的人可以查看官方文件和zkSync的规范。验证函数的要点和限制在validateTransaction函数中,可以实现各种定制逻辑,例如如果账户已经实现了EIP-1271标准,可以直接将EIP-1271中的验证逻辑套用在validateTransaction中,或者参考zkSync官方文档中的多签名账户合约实现。同时,在EIP-4337的VerificationPhase中为了避免DoS威胁,有一些限制(不能涉及外部的操作码以及有限的深度等),在zkSync中也有类似的限制,例如:1.合约逻辑只能触及自己的槽位(如果账户合约的地址为A):-属于地址A的槽位-任何其他地址的槽位A-任何其他地址的槽位keccak256(A||X),即可以直接使用地址作为映射的键(例如映射(address=>value)),也等同于允许访问槽位keccak256(A||X),以实现扩展。例如ERC-20上的代币余额。2.合约逻辑不得使用全局变量,例如block.number执行函数的要点和限制在executeTransaction函数中需要注意的是,如果要执行系统调用(SystemCall),需要确保具有isSystem标志。因为这些系统合约对账户系统的影响非常大,例如增加nonce的唯一方式是与NonceHolder互动,要部署合约必须与ContractDeployer互动,使用isSystem标志可以确保账户开发者有意识地与系统合约互动。然而,建议在实现时可以使用zkSync提供的SystemContractsCaller库,以避免自己处理isSystem标志,并使用其中的systemCallWithPropagatedRevert完成系统调用。上述代码示例中涉及与`DEPLOYER_SYSTEM_CONTRACT`进行交互。帐户开发者最常遇到的系统合约情况是我们要使用帐户来部署一个合约,此时必须与`ContractDeployer`这个系统合约进行交互。在这种情况下,帐户开发者需要与`ContractDeployer`合约进行通信,以确保成功部署合约并执行所需的操作。zkSync时代的费用模型和Paymaster费用和Gas限额zkSync的费用模型与以太坊非常相似,费用代币仍然是ETH。然而,除了基本的计算和写入槽位成本外,与其他Layer2解决方案(如Arbitrum、Optimism)一样,zkSync还需要考虑发布到L1的额外成本(安全费用)。由于发布数据到L1上的燃气价格非常不稳定,因此在每个区块开启(开始记录交易)时,zkSync的Operator会定义以下动态参数:-gasPrice:以gwei为单位的燃气价格,即前文提到的交易对象中的tx.gasprice-gasPerPubdata:在以太坊上发布一个字节的数据所需的燃气数量此外,与EIP-4337不同,zkSync不需要定义三种燃气限制:verificationGas、executionGas和preVerificationGas,而只需要一个gasLimit来包含以上所有费用成本,因此用户需要确保gasLimit足够涵盖Verification阶段、Execution阶段以及上传数据到L1的安全费用等所有费用成本。
这个费用成本包含在前文提到的交易对象中的tx.gaslimit。将这两者相乘(tx.gasprice*tx.gaslimit)就可以得到这笔交易支付给bootloader的手续费数量。PaymasterPaymaster主要在用户交易支付手续费阶段,代替用户的帐户合约向bootloader支付ETH。用户可以选择不同的Paymaster和支付模式来支付手续费,例如(但不限于):-在交易发起前或交易执行后向Paymaster支付ERC-20代币-使用信用卡向Paymaster合约充值-Paymaster将持续为用户免费支付部分或全部手续费用户与Paymaster互动的方式取决于不同的协议,可以是中心化也可以是去中心化;可以在交易前,也可以在交易后;可以使用ERC-20代币也可以使用法定货币,甚至可以是免费的。zkSync的Paymaster合约主要由两个函数组成,分别是validateAndPayForPaymasterTransaction(必需)和postTransaction(可选),两者都只能被bootloader调用:-validateAndPayForPaymasterTransaction是整个Paymaster合约中唯一必须实现的函数。当操作员收到的交易附带Paymaster参数时,表示手续费不由用户的帐户合约支付,而是由Paymaster支付。此时,操作员将调用validateAndPayForPaymasterTransaction来判断该Paymaster是否愿意支付这笔交易的手续费。如果Paymaster同意,该函数将向bootloader发送至少tx.gasprice*tx.gaslimit的ETH。-postTransaction是一个可选函数,通常用于退款(将未使用完的燃气退还给发件人)。然而,当前的zkSync尚不支持此操作。zkSync中的Paymaster在实现了postTransaction后才会执行postTransaction,这一点与EIP-4337不同,EIP-4337在validatePaymasterUserOp没有返回上下文时不会调用postOp,反之亦然。综合以上,举例来说用户现在想要发送一笔手续费由Paymaster支付的交易,那流程如下:借由NonceHolder确认nonce是否合法呼叫用户AccountContract上的validateTransaction进行验证,确认交易由帐户拥有者授权呼叫用户AccountContract上的prepareForPaymaster,里面可能会执行例如approve一定数量的ERC-20Token给Paymaster或是不做任何事呼叫PaymasterContract上的validateAndPayForPaymasterTransaction确认Paymaster愿意支付并且收取手续费,同时Paymaster向用户收取一定数量的ERC-20(前面approve的)确认bootloader收到正确数量(至少tx.gasprice*tx.gaslimit)的ETH手续费呼叫用户AccountContract上的executeTransaction执行用户想要的交易如果PaymasterContract有实作postTransaction且gas仍然足够(没有outofgaserror),那就执行postTransaction最后一步即便outofgaserror导致不能执行postTransaction,这笔AA交易也算是成功,只是省略掉呼叫postTransaction的动作而已。更深入探究zkSync的Paymaster会发现它的VerificationRules和4337稍有不同(zkSyncPaymaster可以踩任何其他合约的slot)、同时也有各种不同的type(例如Approval-based),这部分由于比较细节所以有兴趣深入的人可以参考官方文件或我之前的实作。Summary&Comparison通过前文的解释,我们已经了解了账户合约具有哪些重要的入口点,以及它们的作用和相关限制。同时,我们也了解了系统合约的功能。接下来,让我们对在zkSync中一个自动操作(AA)交易从构建到完成的过程进行总结,同时我也会提供更详细的参考资料,以供那些希望深入了解的人参考:1.用户在本地使用SDK或钱包构建交易对象(例如:from、to、data、value等)。2.用户对该交易进行签名。这里的签名不一定是传统的EIP-712格式和ECDSA曲线签名。
zkSync还支持EIP-2718和EIP-1559,选择签名方式和验证方式的关键在于通过帐户合约中的验证函数进行验证。3.将已签名的交易通过RPCAPI发送给操作员(Operator)。此时交易进入待处理状态。操作员将交易传递给bootloader(调用bootloader合约上的processL2Tx函数),开始一系列的AA协议流程。4.Bootloader会检查Nonce是否合法,使用NonceHolder进行检查。5.Bootloader会调用用户账户合约上的validateTransaction函数,以确认此交易已获得帐户所有者的授权。6.Bootloader收取手续费有两种方式,具体收费方式取决于交易参数(构建交易对象时是否附带paymaster参数):a.调用payForTransaction函数与账户合约收取手续费;b.调用prepareForPaymaster和validateAndPayForPaymasterTransaction函数与Paymaster合约收取手续费。7.「呼叫payForTransaction来跟Account合约手续费」或者「呼叫prepareForPaymaster和validateAndPayForPaymasterTransaction来跟Paymaster合约手续费」8.检查bootloader是否已收到至少tx.gasprice*tx.gaslimit数量的交易手续费。9.Bootloader会调用用户账户合约上的executeTransaction函数来执行交易。10.(可选)如果使用Paymaster支付手续费,bootloader会调用postTransaction函数。如果Paymaster没有实现postTransaction,或者gas已耗尽,将跳过此步骤。以上的4.~7.步为验证阶段(定义在bootloader的l2TxValidation),第8.~9.步执行阶段(定义在bootloader的l2TxExecution)。EIP-4337、StarkNet和zkSync时代的比较基本上这三者的AA机制流程都相仿,皆为验证阶段→手续费机制(由账户合约支付或者Paymaster)→执行阶段,主要差别有:执行AA机制的角色是:在zkSync时代中开启与其他两者AA的差别在于Operator需要和bootloader(系统合约)一起配合,例如bootloader会开启一个新区块并定义该区块的相关参数,接收操作员发送来的交易者并进行验证。在4337中这部分由Bundler与EntryPoint协作,而在StarkNet中这部分全部由Sequencer负责。GasCost是否需要考量到L1安全费用:L2的AA都需要考虑这个上传数据到L1的费用,不只是推送提到的ZK(Validity)RollupsNativeAA,在OptimisticRollups实作4337时也需要算入L1安全费用(计算在preVerificationGas中,细节可见Alchemy相关文件)。是否可以在账户合约部署前发送出交易:在StarkNet和zkSync时代中都没有像4337的EntryPoint有initCode这个字段允许替用户部署账户合约,所以其都不在可以配置账户前发送出交易。对比由于StarkNet尚无已实现的Paymaster机制、zkSync也尚未完成gas退款机制的设计,所以一些比较细节的比较在这里就没有列出。另外,目前的4337bundler我们完成了P2Pmempool,且zkRollups的Sequencer和Operator也还是唯一的官方服务器,所以都有一定中心化的成分存在。在开发流程上zkSync由于没有与各家bundler串接的问题(只需要与OperatorAPI交互),所以使用起来4337很容易,开发帐户合约(SDK)的体验也更好;同时zkSync可以使用Solidity作为合约开发语言,所以也不需要在StarkNet开发中跨过Cairo的门槛。结语由于StarkNet和zkSync都属于本地AA(NativeAA)的范畴,因此你也可以参考我之前撰写的StarkNetAA介绍文章,题为《StarkNetAccountAbstraction简介》(IntroductionofStarkNetAccountAbstraction)。此外,你还可以阅读与EIP-4337相关的其他文章,以获得更多相关信息。