您正在查看: Ethereum 分类下的文章

Uniswap Permit2 - 高效、一致和安全的授权

Uniswap Labs发布了两个新的智能合约Permit2和UniversalRouter, Permit2 确实可以让链上交易体验上一层楼。

前几天,Uniswap Labs发布了两个新的智能合约 Permit2 和 Universal Router :

  1. Permit2 允许代币授权在不同的应用程序中共享和管理,创造一个更统一、更具成本效益、更安全的用户体验。
  2. Universal Router 将ERC20和NFT兑换统一到一个单一的兑换路由器。与Permit2整合后,用户可以在一次兑换交易中兑换多个代币和NFT,同时节省Gas费。

Uniswap最初构思Permit2和Universal Router是为了改进Uniswap自己的产品,优化Gas成本,简化用户交易流程,并加强安全性。在构思的过程中,Unswap觉得其他应用可以从整合这些合约中大大受益。Uniswap 本身致力于建设公共基础设施,因此设计了这些合约,提供整个开发者生态系统使用,包括广泛的文档、SDK。

Permit2是一个代币授权合约,可以在不同的智能合约中安全地共享和管理代币授权。随着越来越多的项目与Permit2集成,可以在所有应用程序中对代币授权进行标准化。反过来,Permit2将通过降低交易成本来改善用户体验,同时提高智能合约的安全性。

典型授权模式

以下是EIP-20中定义的典型代币授权(Approve)方法图示:

  1. Alice在一个ERC20上调用approve(),向一个合约授予支出授权。
  2. Alice在合约上调用一个交互函数,该函数又在ERC20代币合约上调用transferFrom(),转账她的代币。

显然,这种模式是可行的(它无处不在),并且最终可以相当灵活,因为协议通常会最终不间断地长期访问用户的代币。但它有两个众所周知的现实世界的问题。

  • 糟糕的用户体验, 用户必须对他们打算使用的每个代币授权给每个新的协议,这导致了混乱的用户体验,同时这几乎总是一个单独的交易,浪费了Gas和时间。
  • 糟糕的安全性,应用程序通常要求用户授权最大限额,以避免重复上述用户体验问题。这意味着,如果协议被利用,每个用户授权协议使用的代币都有可能被直接从他们的钱包里拿走(因为应用程序可以无限期地访问钱包的整个代币余额)。

授权签名(EIP-2612)模型

EIP-2612 对代币的授权进行了迭代。用户可以通过在他们的交易中附加一个授权签名(Permit)信息来与应用合约交互,而不需要事先授权。
让我们看看ERC20的EIP-2612扩展所启用的方法,它通常是这样的:

  1. Alice签署一个链外的 "permit(签名授权)" 信息,表示她希望授予一个合约一个(EIP-2612)代币的使用权。
  2. Alice提交签署的消息,作为她与所述合约交互的一部分。
  3. 合约调用代币上的 "permit()" 方法,它会使用签名授权信息和签名,同时授予合约一个授权。
  4. 合约现在有了授权,所以它可以在代币上调用transferFrom(),转账由Alice持有的代币。

这解决了典型ERC20 授权方法的两个问题:

  • 用户永远不需要提交一个单独的approve()交易。
  • 不再有悬空授权的必要之恶,因为许可消息授予的是即时授权,通常会立即花费。因此也可以选择一个更合理的授权额度,更重要的是,在签名授权消息可以被使用代币的时间上有一个到期时间。

虽然EIP-2612使代币授权更加安全,但在EIP-2612之前推出的代币并不支持签名授权功能,而且并非所有较新的代币都采用该功能,这就是悲催的现实。因此大多数时候,这种方法不可行。

关于EIP-2612, 我那个登链社区上还有一些文章探讨,可参考这里

Permit2模式

最后,让我们深入探讨Permit2的方法,Permit2 结合了这两种模式,将EIP-2612的用户体验和安全优势扩展到也涵盖了普通的ERC20代币!
为了说明Permit2的革命性,在一个常见的场景中,协议需要转账 Alice持有的代币。

  1. Alice在一个ERC20上调用approve(),典型的方式为的Permit2合约授予一个无限的授权。
  2. Alice签署一个链下 permit2 消息,该消息表明协议合约被允许代表她转账代币。
  3. Alice在协议合约上调用一个交互函数,将签署的 permit2 消息作为参数传入。
  4. 协议合约在Permit2合约上调用 permitTransferFrom(),而Permit2合约又使用其授权(在1中授予)在ERC20合约上调用 "transferFrom()",转账Alice持有的代币。

要求用户首先授予一个明确的授权交易,这似乎是一种倒退。但是,用户不是直接授予协议,而是将其授予规范的Permit2合约。这意味着,如果用户之前已经这样做了,比如说与另一个集成了Permit2的协议进行交互,那么其他每一个协议都可以跳过这个步骤。

这太棒了。

协议不会直接调用ERC20代币上的transferFrom()来执行转账,而是调用规范的Permit2合约上的permitTransferFrom()。Permit2 位于协议和ERC20代币之间,跟踪和验证permit2消息,然后最终使用其授权直接在ERC20上执行transferFrom()调用。这种间接性使得Permit2可以将类似于EIP-2612的好处扩展到每一个现有的ERC20代币上。

同时,像EIP-2612 签名授权信息一样,Permit2 信息也会过期,以限制漏洞的攻击窗口。

集成 Permit2

对于集成Permit2的前端来说,它需要获取一个用户签名,并将其传递到交易中。这些签名签署的Permit2消息结构(PermitTransferFrom)必须符合EIP-712标准(社区有一些相关文章),使用这里这里定义的Permit2域和类型散列。请注意,EIP-712 Permit2对象的 spender 字段需要被设置为将要消费它的合约地址。

智能合约的整合实际上是相当容易的! 任何需要转账用户持有的代币的函数只需要接受任何的许可信息细节和相应的EIP-712用户签名。为了实际转账代币,我们将在规范的Permit2合约上调用permitTransferFrom()。该函数的声明为:

function permitTransferFrom(
    PermitTransferFrom calldata permit,
    SignatureTransferDetails calldata transferDetails,
    address owner,
    bytes calldata signature
) external;

这个函数的参数是:

  • permit - permit2 消息的详情, 有下面的信息。
    • permitted 一个TokenPermissions结构,有以下字段:
      • token - 要转账的代币的地址。
      • amount - 此签名信息可转移的最大金额。
    • nonce - 一个独特的数字,用来防止重用签名许可。一旦签名许可被使用,任何使用该nonce的其他签名许可将无效。
    • deadline - 该签名许可有效的截止时间。
  • transferDetails - 一个包含转账接收人和转账金额的结构,可以小于用户签名的金额。
  • owner - 签署许可的人,也是持有代币。通常,在简单的使用场景中,调用者和用户是同一个人,这应该被设置为调用者(msg.sender)。但在更奇特的集成中,你可能需要更复杂的检查。
  • signature - permit2信息对应的EIP-712签名,由owner签名。如果从签名验证中还原的地址与 owner 不一致,调用将失败。

注意,PermitTransferFrom结构不包括签名信息 EIP-712 typehash 定义中的spender字段。在处理过程中,它将被填入我们的合约地址(permitTransferFrom()的直接调用者)。这就是为什么用户签署的EIP-712对象的spender字段必须是这个合约的地址。

高级集成

前面涵盖了 Permit2 提供的基本功能,但你还可以用它做更多的事情!

  • 自定义见证数据 - 你可以将自定义数据附加到permit2的信息中,这意味着Permit2的签名验证也将扩展到验证这些数据。
  • 批量转账 - 一个用于执行多个转账的批量 permit2 消息,由一个签名来保证。
  • Smart Nonces - 在底层,nonces实际上被写成存储槽中的位字段,并以上面的248位为索引。你可以通过仔细选择重用存储槽的nonce值来节省大量的Gas。
  • 回调签名 - Permit2支持EIP-1271回调签名,它允许智能合约也签署permit2消息。
  • Permit2 Allowances - 对于需要更多灵活性的协议,Permit2支持一个更传统的授权模型,得到了过期时间的额外好处。

The Demo

这里提供的示例代码是一个简单的金库,用户可以使用Permit2将ERC20代币存入其中,随后可以提取。因为它是多用户的,它需要启动转账,以便可靠地记入哪个账户拥有哪个余额。通常情况下,这需要给金库合约授予授权,然后让金库对代币本身执行transferFrom(),但Permit2让我们跳过了这个麻烦!

Test用例部署了一个本地的、字节码的主网Permit2合约的分叉,以测试金库的一个实例。EIP-712 Hash 和签名生成也是用solidity/foundry编写的,但通常应该在链外用你选择的语言在前端或后端执行。

参考资源

转载自:https://learnblockchain.cn/article/5161

名词解释:Web3 账户相关概念大梳理

刚刚结束的 Devcon 上,账户抽象算是是最热的几个话题之一,最近可以经常看到 AA / EOA / SCW / 4337 等缩写和代号在各种 talk、panel 和信息流里出现。再加上叙事开始往「Onboarding next billion users」的方向发展,一些新的形容词也开始出现在产品之前,比如 seedless / gasless / social recovery / non-custodial。相信看完这两句的你已经开始脑壳疼了,那么接下来就让我尽自己所能来帮大家梳理一下这些名词概念到底代表什么。

阅前提示:本文不是严肃的技术文档,可能会用不精确但容易理解的语言进行阐述或比喻,欢迎大家以此为起点深入探索这些技术的细节。

EOA - Externally Owned Accounts

EOA 中文叫做 外部账户,我们最熟悉的 MetaMask 生成的地址就是 EOA。它的特点是原理简单,比如生成规则是:

私钥 → 公钥 → Keccak256 哈希 → 最后 20 Bytes → 十六进制字符串(EOA 地址)

可以看出这个规则非常直接,全是由数学变换计算出来的,生成的地址内部没有任何结构和逻辑。节点验证一笔交易是否被地址 owner 授权的时候也是固定的规则:

交易签名 → ec_recover → 公钥 → (用上面的规则生成)地址 → 对比要操作的地址

对比结果一致那么验签通过,进行后续流程;不通过则直接打回,不会进一步广播交易。

EOA 的另一个设定是作为交易的发起方并支付 gas,相对应的 CA(合约账户) 只能被其他 CA 或者 EOA 调用。也就是说,EOA 是交易的触发器,一笔交易无论后面有多少合约调用,一开始都必须由一个 EOA 发起并且支付足够的 gas 才可以进行。

需要指出的是,EOA 是以太坊以及其他 EVM 兼容链(或类 EVM 链)才有的概念,严格来说包括 BTC 在内的主流非 EVM 链都没有这个设定。

CA - Contract Accounts

CA 中文叫做 合约账户(也曾被称为内部账户),我们常见的 ERC-20 代币合约、DeFi 业务合约等都有一个跟 EOA 长得很像的地址,这就是 CA。

在设定上,CA 是以太坊世界的原住民,EOA 和 ETH 是为 CA 的业务逻辑准备的触发器和燃料;实际使用下来,以太坊上除 ETH 之外的所有资产都是由 CA 承载,DeFi 等业务逻辑就更是全都由 CA 来实现。然而 CA 无法主动进行操作和支付 gas 的设定也限制了它的能力,早在 2016 年就有提案希望能让 CA 自己支付 gas。

简单来说,CA 是具备内部逻辑的以太坊账户,里面既可以是业务逻辑(Token 合约用来记账,质押合约用来放贷和清算),也可以是账户逻辑(比如 gnosis safe 的多签逻辑),而后者就是我们即将提到的「SCW - 智能合约钱包」概念。

CA 的地址规则是通过计算生成的,有 CREATE 和 CREATE2 两种方式,这里不再展开。大家只需要记住 CA 和公钥没有必然对应关系即可,比如 gnosis safe 创建的 CA 里可以设定任意多把公钥来解锁它的地址对应的资产;当然 CA 也可以不设定任何密钥,而是由其他 CA 的逻辑决定是否可以解锁,比如 DeFi 的借贷合约,只要还了钱就能取回质押的资产。

SCW/A - Smart Contract Wallet/Account

智能合约钱包 应该是字面意思最好理解的了,也就是用 CA 作为地址的钱包方案,而我们常用的 EOA 钱包方案是用前述的公钥变换结果作为地址。由于具备内部逻辑,智能合约钱包可以实现很多 EOA 无法实现的功能,比如 gas 代付,批量交易,权限管理,离线授权,社交恢复等等。

这里举几个例子来展示一下智能合约钱包的扩展潜力:

  • Gnosis safe 利用智能合约钱包架构实现多签逻辑;
  • 用户可以在一笔上链交易中同时给多个地址发送不同的 token,也可以在用 uniswap 时让 approve 和 swap 在一笔交易里完成,从而做到需要多少授权多少,避免因为过度授权造成安全隐患。
  • 用户可以给不同资产设定不同的操作权限,比如给 PFP 设定比普通 ERC-20 token 更高的操作门槛(例如需要一把由硬件钱包管理的 admin key 才能转移),这样即便日常使用的环境发生密钥泄露,黑客也无法将高价值资产转走,在安全和便利中间取得平衡。
  • 用户可以签署一个离线授权「谁能给我 100 ETH,就可以转走我的某个 BAYC」,这样不需要授权给第三方合约,用户就可以跟其他人 P2P 地完成原子交易。

AA - Account Abstraction

账户抽象 其实不是一个新概念了,最早可以追溯到 2015 年的一些讨论,当时 Vitalik 认为至少要让以太坊用来验证交易的密码学算法做到可替换,比如换成性能更优的 ed25519(详见这里),可以说 7 年来 Vitalik 和 EF 都没有停止对账户抽象方案的讨论和探索,这里有个整理好的 link tree 可以帮大家回顾一下历史。
那么账户抽象怎么理解呢?这里我引用一下 ERC-4337 里对其目标的描述

Achieve the key goal of account abstraction: allow users to use smart contract wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and EIP-3074 both require)

可以看出以太坊对于账户抽象的期望是改变目前大多数人都在使用 EOA 的现状,希望用户转向 SCW,并且把生态对 EOA 的依赖完全去除。除了里面提到的 EIP-3074 之外,还有一个更为激进和远期的 EIP-5003,这里同样引述几段原文(有省略):

EOAs … are limited by the protocol in a variety of critical ways. These accounts do not support rotating keys for security, batching to save gas, or sponsored transactions to reduce the need to hold ether yourself. There are countless other benefits that come from having a contract account or account abstraction, like choosing one’s own authentication algorithm, setting spending limits, enabling social recovery, allowing key rotation, arbitrarily and transitively delegating capabilities, and just about anything else we can imagine.

…This EIP provides a path not to enshrine EOAs, but to provide a migration path off of them, once and for all.

不难看出,EIP-5003 的目标是一次性将 EOA 转换为 CA,让所有用户用上 SCW,彻底解决向前兼容的问题。(经过上面的名词解释,看这些缩写是不是顺畅了些?)

到这里大家对 AA 的来龙去脉和未来目标应该有所了解了。但需要指出的是,AA 这个概念不是以太坊和 EVM 专属的,很多链原生已经具备了不同程度的 AA 特性。比如 EOS / Polkadot / Near / Solona / Flow / Aptos … 甚至 BTC(单签 / 多签 / Taproot),这些链在设计时就已经将账户做成了有内部结构甚至具备权限管理能力的状态,还有 StarkNet / CKB 等具备更完善的账户抽象能力。说到这里大家不难发现,以太坊的 AA 是在解决 EOA 意外地流行带来的历史遗留问题,从而在账户层面上变得更加先进和灵活。

4337 - ERC 4337

从上面对 AA 的讨论里不难看出,ERC-4337 只是这个方向众多提案中的一个,但是为什么大家一提到 AA 或者 SCW 就会说到它呢?我们来看这个文档的副标题:

An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure.

也就是说,ERC-4337 是 AA 的路线第一次从「暴力革命」转向「和平演变」,不再追求利用共识层的改变实现 AA,而是转而使用 SCW 这种用户层的方案。并且为了实现更好的互操作性,ERC-4337 定义了一些 SCW 应该实现的接口,以及元交易打包、gas 代付等基础设施的框架。它的出现让目前差异极大的各种 SCW 方案能够拥有统一的用户交互界面以及共用一些生态层面搭建的开放基础设施,有助于各种场景快速实现自己需要的 SCW 方案。另一方面,ERC-4337 的推动有助于促进生态其他参与方提升对 SCW 的兼容性,比如验签需要的 EIP-1271 和有些 DeFi 协议里定义的禁止 CA 交互的一些规则。

Seedless

这里的 seed 指的是 seed phrase,就是我们创建钱包的时候经常被要求备份的助记词。那么 seedless 的意思就是「无助记词的」,或者也可以说成「无私钥的」。注意这个「无」并不是实际意义上的没有密钥,而是指不需要用户备份助记词 / 私钥或者感知到它们的存在。

一个常见的问题是,如果用户不备份助记词,用户是不是就没有账户的控制权了?一旦用户切换新设备环境,账户不就无法访问了吗?没错,只是把用户备份助记词的功能砍掉的话只能算是产品设计失误,而 seedless 追求的是用户「不需要」知道助记词的存在,同时依然拥有账户的完全控制权。也就是说,用户(且只有用户自己)拥有在新设备自主恢复账户控制的能力,只是不再依赖助记词这种 UX 很差、过于 geek 的方式,比如下面要讲到的社交恢复就是非常好的一种。

Gasless

这里的 gas 指的是 gas fee,所以 gasless 的意思是「免 gas fee 的」。同样的,gasless 也不是真的不需要支付 gas fee,而是指用户不需要被迫去了解 gas 概念,更不用提前购买各种原生代币来支付 gas。

那么 gas 谁来付?分两种情况:

一种是用户账户里已经有 crypto asset 的时候,比如 play to earn 得到 token,或者领到的空投,亦或是别人的转账,只要这些 token 有一定的价值和流动性,就会有 relayer 愿意接受它们并帮用户支付 gas,以此赚取收益。

另一种是用户账户里没有有价 token,比如刚刚创建的账户。如果此时需要链上交互,应用方可以选择资助用户一些「定向」用途的 gas 来帮他们 bootstrap,从而降低用户流失,这时即便算上 gas 补贴的消耗,整体的用户获取成本反而可能会更低;或者可以通过让用户观看广告等方式来换取一些 gas。这两种策略在 gas 成本较低的 L2 上都非常有效。

Social Recovery

社交恢复 是指利用社交关系帮助用户在丢失密钥的情况下重新获得账户访问权的机制。如果你用微信登录过新设备,应该有过「让你的两个朋友发送 xxx 给你的账号以登录」的体验——这就是社交恢复想达到的效果,只不过验证方从微信变成了智能合约。

一种常见的误区是把利用社交账号来创建 / 登录钱包的方案称为社交恢复,这是错把「社交关系」与「社交平台账号」划了等号。老牌智能合约钱包 Argent 就内置了社交恢复能力,它要求你的 guardian 提供一个以太坊地址,从而在你需要登陆新设备时提供签名来进行授权,然而这一方案的潜在设定就是:你的 guardian 一定比你在管理以太坊账户上更专业,否则当你需要他们签名的时候,如果他们自己的账户已经无法访问,你的账户也会连带遭殃。所以一种更加可行的办法是利用 email 的密码学证明(DKIM Signature)或者电子护照等生活中常见的密码学工具来增强社交恢复方案的实用性。

Non-custodial

非托管 可以说是 crypto 行业最政治正确、也是被滥用最多的概念之一了,因为很多时候各家都会有自己的定义。这里我也分享一下我们对非托管的定义,主要有两方面:

  • 钱包开发商无法擅自操作用户的账户
  • 钱包开发商无法阻止用户操作自己的账户

如果你也认同这两点,那么判断一个钱包是托管、半托管还是非托管就可以直接拿这两个规则去检验了:

不满足 1 → 托管;满足 1 不满足 2 → 半托管;1、2 都满足 → 非托管。

那么知道了是哪种托管程度有什么用吗,用户可能并不 care 背后的原理,只要好用就行了呗!没错,其实我也部分认同这种观点,至少在现在的阶段,行业还没有发展到发生用户认知范式转移的程度。其实我认为三种类型的方案分别适用于不同的场景:

  1. 托管方案 - 适用于交易所、大机构金服、强合规等场景,比如 coinbase 提供的一些服务。特点是用户量少,不需要应对高频交互,而且客单价高,能支撑服务商花费大成本来维护一系列高防系统。
  2. 半托管方案 - 适用于相对高端的个人用户群体。他们明白服务方可以审查自己的交易,并且有能力提前准备备份方案(比如导出私钥),在服务方主动或被动拒绝服务时可以不影响自己的资产安全。这样日常使用时可以享受安全和便利,极端情况下可以保全资产。注意这种方案对服务商的运维能力要求也非常高,毕竟个人用户量大,日常跟各种应用的交互需求也更高,再就是对数据可用性要求高,毕竟一旦丢失服务端保存的数据有可能导致所有没备份的用户永远无法访问账户。
  3. 非托管方案 - 适用于面向 mass adoption 的场景。初听上去可能是反直觉的,但是从成本上讲,非托管方案是唯一能够在低客单价的场景里保证足够的安全性和可用性的方案。如果一个面向大规模用户场景的应用方打算选择上面两种方案,就一定要考虑对方能否为自己的用户群提供足够安全可用的服务,否则一旦内部人员作恶、黑客入侵或不可抗力导致服务停摆,自己的所有用户都会受到牵连,自己的业务也可能因此一蹶不振。历史上的无数次案例都在讲述一个故事,安全无小事,为用户负责就是为自己负责。

MPC - Multi-Party Computation

多方安全计算 跟 零知识证明(ZKP)可以并称当下 Web3 两大「魔法」,一旦跟它们沾边,似乎原来做不到的事情 somehow 就能做了。实际上有些情况是这样的,尤其是 ZKP,可以利用概率换可行性;MPC 则是通过分散控制权来达成风控或者灾备能力。

MPC 其实是一种范式,包含很多技术方案,在目前 Web3 的语境下大都指的是 tss。

TSS - Threshold Signature Scheme

门限签名 是一种分布式多方签名协议,包含分布式密钥生成、签名,以及在不改变公钥的情况下更换私钥碎片的 re-sharing 等算法。

一个 m-n 的 tss 指的是一个公钥对应了 n 个私钥碎片,其中 m 个碎片的联合签名可以被公钥验签成功。不难发现这个逻辑类似于多签(multi-sig),他们的区别主要在公钥的数量上。

举例来说,2-2 的多签是一个门上挂了 2 把锁,必须用两个钥匙把它们都打开才能开门;2-2 的 tss 是一个门上挂了 1 把锁,但是钥匙有两片,合起来用才能打开门。这里为了好理解,描述并不严谨,两把钥匙合成一把其实更符合 Shamir Secret Sharing 算法的情况;tss 算法下的密钥碎片是不会相遇的,而是它们分别签名之后,通过特定算法可以用对应的公钥验签通过。

那么 tss 是不是一定是托管或者非托管的?其实没有必然联系,主要看最终的方案如何设计和取舍。非托管方案要求用户拥有独立操作账户的能力,所以用户必须掌握不少于门限数量的密钥碎片,例如 2-3 的话用户需要掌握 2 片,而 2-2 的方案无法达成非托管,最多可以做到半托管(比如 ZenGo);但是如果用户管理最多的私钥碎片,那么势必会提高对用户能力的要求,很难做到 mass adoption。

写到这里应该把常见的 Web3 账户相关的名词都覆盖到了,数了一下字数也有差不多 5k 了。这么多的内容难免有错误和疏漏的地方,还请大家不吝拍砖,发现问题或者有不同观点直接来 Twitter 找我提就好(@frank_lay2),后面有内容增改或更新我也会在 Twitter 上及时跟大家同步。

原文链接:https://mirror.xyz/zhixian.eth/dACTTYPzEfRcF6jSE_iwJsnbNmN2Ier_NA_TzkZaOeM
转载自:https://learnblockchain.cn/article/4917

Polygon Hermez

在传统货币理论中存在“不可能三角”,即一国无法同时实现货币政策的独立性、汇率稳定与资本自由流动,最多只能同时满足两个目标,而放弃另外一个目标。

相类似,当前的区块链技术也存在“不可能三角”,即无法同时达到可扩展(Scalability)、去中心化(Decentralization)、安全(Security),三者只能得其二。

  • 可扩展性:每秒可以处理大量交易。
  • 去中心化:拥有大量参与区块生产和验证交易的节点。
  • 安全性:获得网络的多数控制权需要非常高昂的成本。

目前很多区块链会在三者中有所权衡,比如以太坊和比特币比较关心的就是去中心化和安全性。而有一些新公链更注重的是可扩展性和安全性。

从比特币创世开始,一直到以太坊网络中Crypto Kitties游戏的出现。主流公链项目最被人诟病的地方就是低下的TPS,以太坊15左右的TPS完全无法给大多数应用提供实时稳定的支持,这与当前互联网行业动辄上万TPS的业务形成了鲜明的对比。
扩展性也许是排在第一位的问题。扩展性问题已经成为很多系统的坟墓。这是一个重大而艰巨的挑战。-Vitalik Buterin

zk-Rollup

对于以太坊而言,过去几年内关于以太坊扩容的方案不断出现。其主流的方案如下所示:

链上扩容

分片(Sharding)技术

Sharding一词本来源于数据库的术语,表示将大型数据库分割为很多更小的、更易管理的部分,从而能够实现更加高效的交互。区块链分片是指对区块链网络进行分片,从而增加其扩展性。根据最新的以太坊2.0规范,以太坊区块链会被分为1024个分片链,这也意味着以太坊的TPS将提高1000倍以上。但目前Sharding方案仍然在跨分片通信、欺诈识别、随机分配与选举安全性等方面存在不足。

链下扩容

状态通道(State Channel)

指代用于执行交易和其他状态更新的“链下”技术。但是,一个状态通道内发生的事务仍保持了很高的安全性和不可更改性。如果出现任何问题,我们仍然可以回溯到链上交易中确定的稳定版本。

侧链(Sidechain)技术

侧链是平行于主链的一条链,由侧链上的验证者把一条链的最新状态提交给主链上的智能合约,这样持续推进的一类系统。侧链通常使用PoA(Proof-of-Authority)、PoS(Proof of Stake)等高效的共识算法。它的优势在于代码和数据与主链独立,不会增加主链的负担,缺陷在于它的安全性弱、不够中心化,无法提供审查抗性、终局性和资金所有权保证。

Rollup技术

顾名思义,就是把一堆交易卷(Rollup)起来汇总成一个交易,所有接收到这个交易的节点只去验证执行结果,而不会验证逻辑。因此Rollup交易所需Gas费会远小于交易Gas费总和,TPS也增加了。主流的Rollup技术可以分为两类:

zk-Rollup

基于零知识证明的Layer2扩容方案,采用有效性验证方法(VP),默认所有交易都是不诚实的,只有通过有效性验证才会被接受。ZkRollup在链下进行复杂的计算和证明的生成,链上进行证明的校验并存储部分数据保证数据可用性。

Optimistic Rollup

乐观的Rollup协议,采用欺诈证明方法,即对链上发布的所有Rollup区块都保持乐观态度并假设其有效,它仅在欺诈发生的情况下提供证据。乐观Rollup的优势在于能使得原生Layer1上的solidity合约可以无缝移植到Layer2,从而最大程度提升了技术人员的研发体验,目前主流方案包括Optimism和Arbitrum。

Plasma方案

通过智能合约和Merkle树建立子链,每个子链都是一个可定制的智能合约,子链共存并独立运行,从而大幅降低主链的TPS压力。

从中长期来看,随着 ZK-SNARK 技术的改进,ZK rollups 将在所有用例中胜出。— Vitalik Buterin

zkEVM

ZK-Rollup早期为人诟病的地方是不能兼容 EVM,不能支持智能合约功能,例如 Gitcoin 捐赠主要支付途径的 zkSync 1.0 仅能支持转账等基本功能。同时,由于不同 ZK 应用有各种专用电路,无法相互调用,可组合性差。因此市场急需能够支持以太坊智能合约的ZK-Rollup,而其中关键门槛就是能够支持零知识证明的虚拟机。随着引入 EVM 兼容的 zkVM,zk-rollups 才开始支持以太坊 dApps。

Comparison

由于 ZK-EVM 并没有统一的设计标准,所以每个项目方基于不同角度在兼容 EVM 和支持 ZK 之间权衡设计出各自方案,目前基本分为两种思路:

  1. 编程语言层面支持,自定义 EVM 操作码,把 ZK-friendly 的操作抽出来重新设计新的、架构不同的虚拟机,通过编译器将 Soilidity 编译成新的虚拟机操作码
  2. 字节码层面支持,支持原生 EVM 操作码

对于第一种策略,由于不受原有 EVM 指令集的约束,可以更灵活的将代码编译成对零知识证明更友好的指令集,同时也摆脱了兼容所有 EVM 原有指令集所需要的艰巨而繁重的工作。

对于第二种策略,由于完全支持了 EVM 现有的指令集,其使用的是和 EVM 一样的编译器,因此天然就对现有的生态系统和开发工具完全兼容,同时还更好的继承了以太坊的安全模型。

第一种思路更灵活,工作量更小,但需要花费额外精力在适配上;第二种思路工作量相对来说会大一些,但是兼容性更好,安全性更高。

Starkware zkEVM

Starkware 的 ZK-Rollup 通用解决方案 StarkNet 可以运行任意的以太坊 dApp。开发者可以通过编译器将 Solidity 编译成 StarkNet 的智能合约语言 Cairo,再部署到其 ZK-friendly 的 VM。

zkSync zkEVM

类似 Starkware,zkSync 2.0 通过开发编译器前端 Yul 和 Zinc 来实现 ZK-EVM 功能。Yul 是一种中间 Solidity 表示,可以编译为不同后端的字节码。Zinc 是用于智能合约和通用零知识证明电路的基于 Rust 的语言。它们都是基于开源框架 LLVM,能够实现最高效的 ZK-EVM 字节码。

https://miro.medium.com/max/1400/0*S3TKmlfGRTx5MNkE
与 StarkNet 一样,zkSync zkEVM 在语言层面实现了 EVM 的兼容性,而不是在字节码层面。

Polygon zkEVM

Polygon Hermez 是一个具有 zkVM 的 Polygon zk-rollup,旨在支持 EVM 的兼容性。为此,EVM 字节码被编译成 「微操作码(micro opcodes)」 并在 uVM 中执行,uVM 使用 SNARK 证明和 STARK 证明来验证程序执行的正确性。

Scroll zkEVM

Scroll 是一个EVM等效的zk-Rollup,可以实现与以太坊字节码级别的兼容性,也就是说,所有的EVM操作码和基础层完全相同。Scroll 团队计划为每个 EVM 操作码设计零知识电路。

https://github.com/privacy-scaling-explorations/zkevm-circuits

"Scroll design, architecture, and challenges" (Ye Zhang, Scroll)

Which is better?

https://vitalik.ca/general/2022/08/04/zkevm.html
在 Vitalik 的博文里,他将 ZK-EVM 分为几种类型。其中,类型 1 是直接在以太坊上面直接开发 ZK-EVM,这个开发过于复杂而且目前效率太低,以太坊基金正在研究中。类型 2、类型 2.5 和类型 3 是 EVM 等效的 ZK-EVM,Scroll 和 Polygon Hermez 目前处于类型 3 这个阶段,朝着类型 2.5 乃至类型 2 努力。类型 4 是高级语言兼容的 ZK-EVM,包括 Starkware 和 zkSync。这些类型并无好坏之分,而且 ZK-EVM 也没有统一的标准。

从理论上讲,以太坊不需要为 L1 使用单一的 ZK-EVM 实现进行标准化;不同的客户可以使用不同的证明,因此我们继续从代码冗余中受益。— Vitalik Buterin

Overview

Polygon zkEVM主要包含以下组件:

Proof of Efficiency (PoE) Consensus Mechanism

  • zkNode
    • Synchronizer
    • Sequencers & Aggregators
    • RPC
  • zkProver
  • Bridge

PoE

https://wiki.polygon.technology/ko/assets/images/fig2-simple-poe-7cb9c3761a3d3c6482eefba525598cd2.png
Proof-of-Efficiency(PoE)共识算法分2步实现,由不同参与者完成:

1)第一步的参与者称为Sequencer。sequencer负责将L2的交易打包为batches并添加到L1的PoE智能合约中。任何运行zkEVM-Node的参与者,均可成为Sequencer。每个Sequencer都必须以$Matic token来作为抵押物,以此来获得创建和提交batches的权利。Sequencer赚L2的交易手续费,但是只有相应的证明提交后,Sequencer才能获得其所提交的batch内的L2交易手续费。
2)第二步的参与者称为Aggregator。Aggregator负责检查batches的有效性,并提供相应的证明。作为Aggregator,需要运行zkEVM-Node和zkProver来创建相应的零知识证明,赚取Sequencer为batches支付的Matic费用。

PoE智能合约有2个基本的函数:
1)sendBatch:用于接收Sequencer提交的batches。
2)validateBatch:用于接收Aggregator生成的proof,并进行验证。

zkEVM-Node

Basic concept

https://wiki.polygon.technology/ko/assets/images/fig3-zkNode-arch-aa4d18996fba1849291ea18e3f11d955.png

  • Batch: 为一组使用zkProver来执行或证明的交易,会将batch发送到L1,也会从L1同步batch。
  • L2 Block: 目前,所有的L2 block被设置成只包含一个交易,从来能够保证即时的确认。
  • RPC:为用户(如metamask、etherscan等)与节点交互的接口。与以太坊RPC完全兼容,并附加了一些额外的端口。如与state交互可获得数据的接口;处理交易的接口;与pool交互存储交易的接口。
  • Pool:通过RPC来存储交易的DB,pool中所存储的交易后续可由sequencer来选中或丢弃。
  • Sequencer:
    • Sequencer 从用户那里接收 L2 交易,将它们预处理为新的 L2 batch,然后将该 batch 作为有效的 L2 交易提交给 PoE 合约。 Sequencer 接收来自用户的交易,并将收取所有已发布交易的交易费。 因此,Sequencer 在经济上受到激励来发布有效交易,以便从中获得最大利润。
    • Trusted sequencer: 具有特殊权限的Sequencer。这样做是为了实现快速最终确定并降低与使用网络相关的成本(较低的gas)。
    • Permissionless sequencer:任何人都可以参与,但是会带来较慢的最终确认等问题,相应的,抗审查性和去中心化会好一些。目前尚未实现。
  • Aggregator:通过生成零知识证明来验证之前提交的batches。它会通过向state发送请求来获得prover所需输入数据。一旦proof生成,即可发送到L1中的智能合约进行验证。
  • Synchronizer:负责从以太坊区块链读取事件,通过etherman从以太坊获取数据更新state。Synchronizer从智能合约中获取的数据包括Sequencer发布的数据(交易)和Aggregator发布的数据(有效性证明)。所有这些数据都存储在一个巨大的数据库中,并通过JSON-RPC服务提供给第三方。
  • Prover:生成ZK proofs的服务。注意Prover并未在zkEVM-Node中实现,而是从节点的角度将其当成是“黑盒”。当前Prover有2版实现:
    • JS参考版本实现——zkevm-proverjs库
    • C++生产版本实现——zkevm-prover库
  • Etherman:对需与以太坊网络和相关合约交互的方法的抽象。
  • State:负责管理存储在StateDB的状态数据(batches、blocks、transactions等),同时State还会处理与executor和Merkletree服务的集成。
  • StateDB:为状态数据的持久层。
  • Merkletree:该服务中存储Merkle tree,包含了所有的账号信息(如balances、nonces、smart contract code 和 smart contract storage)。Merkletree模块也并未在zkEVM-Node中实现,而是作为节点的一个外部服务实现在zkevm-prover中。

Source Code

./zkevm-node run --genesis ../config/environments/local/local.genesis.config.json --cfg ../config/environments/local/local.node.config.toml --components synchronizer

Sequencer

每个Sequencer都有一个配置,池子,状态,交易管理者,etherman,gpe等

type Sequencer struct {
   cfg Config

   pool      txPool
   state     stateInterface
   txManager txManager
   etherman  etherman
   checker   *profitabilitychecker.Checker
   gpe       gasPriceEstimator

   address common.Address

   sequenceInProgress types.Sequence
}

start

  1. 循环调用isSynced,判断synchronizer是否同步完成
    1. 查询数据库state.virtual_batch执行sql获取lastSyncedBatchNum
    2. 调用接口从PoE合约中获取lastEthBatchNum
    3. 如果lastSyncedBatchNum<lastEthBatchNum,说明还没同步完成
  2. 同步完成后,初始化sequence,获取最新的batchNum
    1. 如果batchNum为0,创建创世区块
    2. 否则执行loadSequenceFromState,确定Sequencer中的sequenceInProgress,即确定一个序列
  3. 启动一个协程,执行trackOldTxs,将pool中已处理过的交易在数据库中删除
  4. 启动一个协程,执行tryToProcessTx来对交易进行处理
  5. 启动一个协程,执行tryToSendSequence,将sequence发送到L1

loadSequenceFromState

  1. 循环检查是否同步完成
  2. 执行MarkReorgedTxsAsPending,将重组的交易的状态从selected更新为pending
  3. 获取最新信息lastBatch和这个块是否被关闭了,如果关闭了:
    1. 开始一个状态交易
    2. 获取最新的global exit root,并构造一个进行时的上下文
    3. 调用OpenBatch将新的batch加入state中,batchnumber+1
    4. 更新Sequencer中的sequenceInProgress
  4. 否则,如果没有关闭
    1. 根据batchNumber获取所有交易
    2. 新建一个Sequence并设为当前Sequencer处理中的Sequence

trackOldTxs

  1. 获取即将要从pool中删除的交易的txHashes
  2. 根据txHashes中的hash,直接在数据库中删除

tryToProcessTx

  1. 检查同步
  2. 检查当前sequence是否应该被关闭,即batch中的交易数量是否超过了最大限制
  3. 备份当前sequence
  4. 处理sequenceInProgress中的txs
  5. 更新状态

tryToSendSequence

  1. 检查同步
  2. 获取需要被发送给L1的sequence数组
  3. 调用SequenceBatches,重试发送sequences给eth

Aggregator

type Aggregator struct {
    cfg Config

    State                stateInterface
    EthTxManager         ethTxManager
    Ethman               etherman
    ProverClients        []proverClientInterface
    ProfitabilityChecker aggregatorTxProfitabilityChecker
}

start

  1. aggregator中会建立很多对Prover的RPC连接,对aggregator中的每一个proverClient,都启动一个协程,调用tryVerifyBatch
  2. 启动一个协程,循环调用tryToSendVerifiedBatch

tryVerifyBatch

  1. 检查网络是否同步
  2. 调用getBatchToVerify获取需要被verify的state.Batch
  3. 根据获取的batch调用buildInputProver构建一个inputProver
  4. 在proverClients中找一个闲置的prover
  5. 调用GetGenProofID,这个函数会根据上面的inputProver生成一个genProofRequest,并调用prover的genProof生成一个proof,返回一个proofID
  6. 执行sql语句,将proof插入到state里面
  7. 调用getAndStoreProof,获取proof.Proof并更新

tryToSendVerifiedBatch

  1. 检查是否同步
  2. 调用GetLastVerifiedBatch获取最新的确认过的lastVerifiedBatch
  3. 调用GetGeneratedProofByBatchNumber获取proof
  4. 如果proof不为空,调用VerifyBatch,将proof发送给智能合约
  5. 成功后再删除proof

VerifyBatch

最后会调用POE的VerifyBatch进行验证

Synchronizer

Sync

  1. 获取dbTx
  2. 获取最新的区块lastEthBlockSynced,如果获取不到,用genesis区块
  3. commit dbTx
  4. 循环进行同步
    1. 调用syncBlocks从特定的块同步到最新块
    2. 从合约获取最新batch number:latestSequencedBatchNumber
    3. 从state获取latestSyncedBatch
    4. 如果latestSyncedBatch >= latestSequencedBatchNumber说明,合约的状态全部同步完成,就调用syncTrustedState

RPC

NewServer

在调用NewServer的时候,会调用registerService注册serviceName和service,保存在serviceMap中,serviceMap中包含service和funcMap
具体的:例如eth服务ethEndpoints,会遍历ethEndpoints的所有方法,并保存在funcMap中。
目前的serviceName:

const (
   // APIEth represents the eth API prefix.
   APIEth = "eth"
   // APINet represents the net API prefix.
   APINet = "net"
   // APIDebug represents the debug API prefix.
   APIDebug = "debug"
   // APIZKEVM represents the zkevm API prefix.
   APIZKEVM = "zkevm"
   // APITxPool represents the txpool API prefix.
   APITxPool = "txpool"
   // APIWeb3 represents the web3 API prefix.
   APIWeb3 = "web3"
)

start

  1. 初始化json rpc server,包括host和port
  2. 调用handle处理请求,从request中解析出serviceName和funcName,再从serviceMap和funcMap中获取service和funcData并调用相关函数进行处理,获取结果

zkProver

Polygon zkEVM 中交易的证明全部由zkProver来处理。通过电路来保证交易执行的有效性。zkProver 以多项式和汇编语言的形式执行复杂的数学计算,随后在智能合约上进行验证。

Interation with Node and Database

如上面的流程图所示,整个交互分为4步:

  1. 节点将 Merkle 树的内容发送到数据库以存储在那里
  2. 节点然后将输入交易发送到 zkProver
  3. zkProver 访问数据库并获取生成证明所需的信息。 这些信息包括Merkle树根、相关siblings的键和hash等
  4. zkProver 然后生成交易证明,并将这些证明发送回节点

Components


而zkProver的内部可以看作由以下四个部分组成:

Exector

Executor其实就是Main State Machine Executor。它将交易、新旧状态根等作为输入。
同时Exector还使用:

  1. PIL(Polynomial Identity Language)和一些寄存器
  2. ROM:存储与执行相关的指令列表。

有了这些,Executor会生成承诺多项式和一些公共数据,这些公共数据构成了 zk-SNARK 验证器输入的一部分。

STARK Recursion

Main State Machine Executor将交易和相关数据转换为承诺多项式之后会作为STARK 递归组件的输入:

  • 承诺多项式
  • 常数多项式
  • 脚本,它是指令列表,为了生成 zk-STARK 证明

为了促进快速 zk-STARK 证明,STARK 递归组件使用Fast Reed-Solomon Interactive Oracle Proofs of Proximity (RS-IOPP),也称为 FRI,用于每个 zk-proof。

Circom Library

最初的Circom论文将其描述为定义算术电路的电路编程语言和编译器。

  1. 包含一组关联的 Rank-1 约束系统 (R1CS) 约束的文件
  2. 一个程序(用 C++ 或 WebAssembly 编写),用于有效地计算对算术电路所有连线的valid assignment。

STARK 递归组件生成的单个 zk-STARK 证明会作为 Circom 组件的输入。
Circom是 zkProver 中使用的电路库,用于为 STARK 递归组件生成的 zk-STARK 证明生成witness。

zk-SNARK Prover

最后一个组件是 zk-SNARK Prover,或者说Rapid SNARK。

Rapid SNARK是一个 zk-SNARK 证明生成器,用 C++ 和英特尔汇编语言编写,可以非常快速地生成Circom输出的证明。目前支持PLONK/Groth16.

之所以采用两套证明系统是因为STARK 证明的生成速度更快,但是证明的规模却很大,在链上验证的时候开销也很大,SNARK 由于更小的证明规模和更快的验证速度,所以在以太坊上验证会更便宜。

https://github.com/matter-labs/awesome-zero-knowledge-proofs
Polygon Hermez zkEVM 使用一个 STARK 证明电路来生成状态转换的有效性证明,用 SNARK 证明验证 STARK 证明的正确性(可以认为是生成「证明的证明」),并将 SNARK 证明提交到以太坊进行验证。

State Machines

zkProver 遵循模块化设计,包含14个状态机,分别对应不同的一些操作:

  • The Main State Machine
  • Secondary state machines:The Binary SM, The Storage SM, The Memory SM, The Arithmetic SM, The Keccak Function SM, The PoseidonG SM
  • Auxiliary state machines:The Padding-PG SM, The Padding-KK SM, The Nine2One SM, The Memory Align SM, The Norm Gate SM, The Byte4 SM, The ROM SM

How to run
Requirement
zkEVM-Prover: 128vCPU, 1T RAM Recommended by Hermez Team

Config
zkEVM-Node config: https://github.com/0xPolygonHermez/zkevm-node/blob/develop/config/environments/public/public.node.config.toml

zkEVM-Prover config:https://github.com/0xPolygonHermez/zkevm-prover/blob/main/config/config_prover.json

Notes

  • prover如果配置了statedb为local的话会把数据暂时存在内存中,没有做持久化,一旦重启prover会丢失数据。
  • 在zkevm-contrant下deployment/deployment_v2-0/deploy_parameters.json 中配置好trustedSequencerAddress,保证zkevm-node/config/environments中的node.config.toml中etherman中的PrivateKeyPath配置项的keystore文件对应的是该地址的私钥,否则Sequencer提交交易会报错。
  • trustedSequencerAddress不要用hermez官方仓库的私钥对应的地址。
  • 如果修改了l2的genesis相关配置需要保证合约目录下的deployment/deployment_v2-0/genesis.json和node目录下的config/envirment/中的genesis对应起来。

Resources
Github:https://github.com/0xPolygonHermez

Docs:
https://wiki.polygon.technology/ko/docs/zkEVM/introduction
https://docs.hermez.io/zkEVM/Overview/Overview/

转载自:https://learnblockchain.cn/article/5336

uniswap基本概念

什么是Uniswap?

uniswap是一个进行自动化做市商的项目,该项目的特点是公平,去中心,抗审查,安全。并且uniswap并不会存在特殊群体,参与项目的每个人都是平等的不论你是LP还是Trader。

V1的特点:

  • 支持不同的ERC20token进行交换
  • 可以加入流动矿池成为LP并获取奖励费用
  • 利用公式进行自动定价,每次交易过后都会进行计算定价
  • 支持私人定制的交换

每个LP按照一定比例输入ERC20-ERC20的数量之后,会获得一定数量LPtoken,用来表示贡献度,可以根据贡献度来领取池中的奖励(奖励的来源为每次交易收取的费用)。如果想退出的话,可以将LPtoken进行销毁,销毁后会按照LPtoken的比例将两种资金进行返回。

同时可以设置限价单和限时单。

Uniswap如何进行计算

无交易费推导

在整个计算过程中uniswap使用的是x-y-k的模型,即为无论怎样进行交易,保持交易后x * y = k,两种代币的乘积不变。根据这个思想,进行推导

设要用Δx数量的x代币交换Δy代币,则有

(x + Δx) * (y - Δy) = k = x * y
设 α = Δx / x, β = Δy / y,将其代入上式可得
∴ (x + α * x) * (y - β * y) = x * y
∴ (1 + α) * (1 - β) = 1
∴ α = β / (1 - β), β = α / (1 + α)
∴ Δy = α / (1 + α) * y = Δx / (x + Δx) * y
  Δx = β / (1 - β) * x = Δy / (y - Δy) * x

带手续费交易

在实际情况中,每一次交易都会收取一定的手续费用来交易LP,通常手续费为交易量的0.3%,这就表明你输入的Δx并不是全用于计算,实际的计算值为Δx * 0.97%,剩下的作为手续费放入交易池中奖励LP,下面是实际的推导过程:
设要用Δx数量的x代币交换Δy代币,则有

设手续费的比例为ρ, 1 - ρ为γ
(x + Δx * γ) * (y - Δy) = k = x * y
设 α = Δx / x, β = Δy / y,将其代入上式可得
∴ (x + α * x * γ) * (y - β * y) = x * y
∴ (1 + α * γ) * (1 - β) = 1
∴ α = β / ((1 - β) * γ), β = (α * γ) / (1 + α * γ)
∴ Δy = (α * γ / (1 + α * γ)) * y = ((Δx * γ) / (x + Δx * γ)) * y
  Δx = β / (1 - β) * x = Δy / (y - Δy) * (1 / γ) * x
//我们假设ρ = 0.3%,所以γ = 997 / 1000
∴ 上述结果可以表示为:
  Δy = (997 * Δx * y) / (1000 * x + 997 * Δx)
  Δx = (1000 * Δy * x) / ((y - Δy) * 997))

通过上面推导可以看出当ρ为0时就成为了无手续费模式,并且可以发现一个问题,代入手续费之后整个池子的k只会略微变大,这是因为会有部分的费用作为手续费进入池子并不会进入交易当中。也可以理解为,你输入的Δx = 手续费 + 实际的交易的数量

在实际的交易中,会出现以下两种情况:

  • 给出交易的x代币数量,计算出y代币的数量
    在这种情况下,输入的x代币会有一部分作为手续费放入池中,其余部分才会被用来做交换。计算时可参考上面手续费交易的结果。
  • 给出想要的y代币数量,计算出所需要的x代币数量
    给出想要获取的y代币,计算所需的x代币数量,同时x代币中包含了手续费和实际交换数量。计算时可参考上面手续费交易的结果。

流动性计算

用户不仅可以进行代币的交换,同时还可以成为LP(Liquidity provider),获取LPtoken用来获取池子中的利息。流动性计算的推导如下

设l = x * y表示两种代币的数量,则有
//提供流动性推导
设α = Δx / x,则有
∴ Δy = Δx / x * y + 1
  Δl = Δx / x * y
∴ x' = x + Δx
  y' = y + Δx * t / x + 1 //这是考虑到solidity语法在计算时的小数会进行向下取整
  l' = l + Δx * l / x

//取消流动性
设β = Δl / l
∴ Δx = Δl * x / l
  Δy = Δl * y / l
∴ x' = x - Δx
  y' = y - Δy
  l' = l - Δl

由于存在向下取整的计算方式,我们将提供和取消两种结合起来看之后会发现,在取消之后剩余的x,y的数量大于提供流动性之前,这是为了保证避免投资者通过这种方式进行获利。如果不使用向下取整的计算方式的话,其实提供和取消之后x,y的数量不会发生变化。同时在提供流动性之后,LP会获取LPtoken,LPtoken数量等于两种代币之积(代币数量更新之后)再开根号。

滑点问题

由于Uniswap是在区块链上的操作,所以可能会导致你看到的价格和实际的价格会有所不同,这是由于交易的确认需要时间,并且交易的顺序不清楚。这样就会导致产生交易滑点,通常为0.5%的滑点保护。而对于滑点的计算我们通常使用(实际成交价格 - 交易时输入的价格) / 交易时输入的价格。当计算出的值满足设定的比例时,即可完成交易。

手续费问题

由于在每笔交易时都会收取一定的手续费,这些手续费会按照LP的持有的token的比例进行分发。这是为了激励用户成为LP并且投入更多的资金,创造更多的流动性。从理论上来说当每一一笔交易发生的时候要将手续费分发给LP,在进行分发的时候可能会使用一个大循环进行分配。但是在实际中这样对用户消耗的gas是很多的,所以这样的方法是不可行的。所以在代码中将手续费的分配放在LP提供流动性和移除流动性的部分,并且维持手续费公平分配的与那里很简单。每次用户进行交易的时候交易的手续费会将每单位的LPtoken的代币的价值提高,而LP在提供流动性的时候会按照当前每单位的LPtoken的代币的价值进行买入LPtoken(变相地理解),举例说明:用户A提供流动性的时候获取了100LPtoken,池子中有1000tokenA,1000tokenB,这时候1LPtoken的价值分别是10tokenA和10tokenB,在经过多次买卖后池子里面有1100tokenA和1500tokenB,这时候1LPtoken对应11tokenA和15tokenB,多出来的这部分即为手续费收益。当用户B想要增加流动性的时候,会按照1LPtoken对应11tokenA和15tokenB的比例进行生成LPtoken,然后按照上面所说的再进行手续费的收益。

质押性挖矿

质押挖矿,在项目中LP可以通过质押自己的LPtoken通过质押一定的时间可以获取质押合约中的奖励代币。再uniswap中LP可以质押自己的LPtoken去获取UNI代币,获取的UNI代币可以去交易所兑换其他的代币,也可以用于参与uniswap的治理,可以通过持有的UNI数量进行投票。

质押挖矿的算法推导

在质押挖矿合约中会进行规定每经过一段时间就会生成一定数量的奖励代币,并且将这些奖励代币按照LP质押的token的数量进行分配给LP。正常思路下我们在分配的时候,会采用循环将质押的用户进行遍历分配。但是在智能合约中使用循环便利的话会消耗大量的gas,这样是不明智的。所以我们要换一种思路来进行奖励的分配。

我们假设质押合约中每秒的奖励为R,合约中质押的代币总数为T,用户A的质押代币数量为a,T包含a
∴ 每秒每一代币的奖励数量为R / T, 用户A每秒获取的代币数量为 a * R / T
我们假设用户A在6秒后取出质押的代币
∴ A所获得的奖励代币数量为:a * R / T * 6

我们再假设B用户在A用户质押4秒后质押了b数量的代币
∴ A所获得的奖励代币数量为:
  a * R / T * 4 + a * R / (T + b) * 2 = a * (R / T + R / T + R / T + R / T + R / (T + b) + R / (T + b))

按照这种思路,我们再进行假设
假设质押的总时长为6秒,A用户在第2秒质押a数量代币,B用户在第4秒质押b数量代币,A用户质押前的代币总数为T'
∴ A所获得的奖励代币数量为:
  a * R / T * 2 + a * R / (T + b) * 2 = a * (R / T + R / T + R / (T + b) + R / (T + b)) = a * (R / T' + R / T' + R / T + R / T + R / (T + b) + R / (T + b)) - a  * (R / T' + R / T')

通过最终的推导式,我们可以得到用户A获得的奖励代币数量等于结算时的累计每份质押代币对应的奖励代币数量之和减去加入时累计每份质押代币对应的奖励代币数量之和再乘以A代币的数量。这样计算的前提是用户A的质押代币的数量不会进行更改,那么问题来了,如果用户对质押代币的数量进行更改了,如何进行计算呢?
其实解决方法很简单,在每次更改数量的时候,对先前数量的质押代币获取的奖励进行结算,然后将变换后的质押代币数量重新进行上述的数量不变的推导。这就是uniswap质押合约代码的逻辑结构,详细的代码可以参考https://github.com/Uniswap/liquidity-staker

问题

  1. 质押挖矿的算法推导是不存在问题的,但是在使用时要结合实际情况来进行考虑。由于solidity中对于除法计算使用的是向下取整,所以在上述推导公式中对于每份质押代币对应的奖励代币数量的计算可能会出现结果为0的情况,虽然uniswap的代码中将每秒奖励的数量扩大10**18倍,但是还要考虑扩大之后如果还是小于总的质押代币数量,这个时候可能会出现用户在这段时间的收益为0的情况。这是由于语言特性产生的bug问题。
  2. 也要考虑额质押代币数量为0的情况,此时计算收益会出现问题。

转载自:https://learnblockchain.cn/article/5346

默克尔树(Merkle Patricia Tree)详解

一、概念

本文是阅读《深入以太坊智能合约开发》的附录A Merkle Patricia Tree后的知识归纳以及扩展。
默克尔树(Merkle Patricia Tree)在以太坊中是一种通用的,用来存储键值对的数据结构,可以简称为“MPT”,是字典树Redix tree的变种,也是以太坊的核心算法之一。
MPT对于树中节点的插入、查找、删除操作,这种结构可以提供对数级别的复杂度O(log(N)),所以它是一种相对高效的存储结构。

二、如何根据键值对构造默克尔树

2.1 节点类型

树类型的数据结构,都会有节点的概念,MPT也不例外。
MPT中有三种节点类型:branch、leaf、extension:

  1. branch(分支)
    1. 由17个元素组成的元组,格式为:(v0,……,v15,vt)。
    2. 其中,v0~v15的元素是以其索引值(0x0~0xf)为路径的子节点数据的keccak256哈希值,如果没有子节点数据则元素为空。
    3. vt为根节点到当前节点的父节点所经过的路径对应的value值,也就是根节点到父节点所经过的路径组成了一个键key,这个key对应的value存在vt里面,如果这个key没有对应的value,那么vt为空。
  2. leaf(叶)
    1. 两个元素组成的元组,格式为:(encodePath,value)
    2. encodedPath为当前节点路径的十六进制前缀编码
    3. value是从根节点到当前节点路径组成的键对应的值
  3. extension(扩展)
    1. 两个元素组成的元组,格式为:(encodePath,key)
    2. encodedPath为当前节点路径的十六进制前缀编码
    3. key为当前节点子节点数据的keccak256哈希值

总结一下上述的那一堆概念:

  • 键值对
  • extension节点记录着:路径、子节点的哈希值。
  • leaf节点记录着:路径、vlaue。
  • branch记录着:以索引值为路径的子节点的哈希值、从根节点到branch的父节点路径组成的键对应的value。
  • value值在leaf和branch节点中存放,key被拆解开,最终由extension、leaf的encodePath以及branch的索引值组成。

2.2 十六进制前缀编码

branch和extension元组的第一个元素encodePath就是当前节点路径的十六进制前缀编码(Hex-Pretix Encoding,HP编码)。使用HP编码能够区分节点是扩展结点还是叶子节点。
而HP编码,和当前节点类型还有当前路径半字节长度的奇偶有关。

半字节是4位二进制,即1位16进制。
0xf是半字节,0xff是1字节。
1111是半字节,11111111是1字节。

共有四种前缀:

所以extension节点有两种前缀:0x00、0x1;leaf有两种前缀:0x20、0x3。
可以看到最终前缀在偶数个半字节0x0、0x2后补了一个0,变成了0x00,0x20,目的是为了凑成整字节,避免出现半字节导致长度不便于合并。
HP前缀需要放在原始路径前面去组成HP编码,实例:

2.3 构造一颗默克尔树

上面的概念不容易理解,现在我们以下面的例子,一步步来进行树的构造,帮助我们更好的理解:
我们假设有一组(4个)键值对数据需要用树来存储:

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

为方便解释说明以及阅读,我们把键值对数据的“键”表示为十六进制字符串,“值”则保留为原始字符串。在实际使用时,它们都需要经过特定的编码变换。

1.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

每棵树都有根节点,默克尔树的根节点会保存当前路径和子节点哈希,所以很明显,根节点会是一个extension节点。
上面节点类型介绍了extension格式为:(encodePath,key),encodePath是十六进制的HP编码。分析给出的4个键我们可以得出都是以6开头,后面分为4、8两条路。所以根节点存储的共同路径值为0x6。
由于0x6只有一位,所以路径长度是奇数,节点又是extension类型,所以HP前缀是0x1,组合出来的HP编码:0x16。
所以当前默克尔树如下图:

HashA代表着子节点的哈希值。

2.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

根节点已经找到,但在根节点后出现了两条路,这个时候需要使用branch来处理这种多条路径的情况。

上文说到,branch由17个元素组成的元组,格式为:(v0,……,v15,vt)。其中,v0~v15是以其索引值(0x0~0xf)为路径的子节点数据的keccak256哈希值,如果没有子节点数据则为空。
这里4和8就是索引值,4、8对应元素是其字节点的哈希值。
所以当前默克尔树如下图:

3.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

我们可以观察到,在0x68后只有唯一路径了,即0x6f727356,而value为“stallion”,所以不再分叉的情况下,就不是branch或者extension了,而应该是一个叶节点。

上文提到,leaf节点是两个元素组成的元组,格式为:(encodePath,value),encodedPath为当前节点路径的十六进制前缀编码,value是从根节点到当前节点路径组成的键,所对应的值。

当前节点的路径是0x6f727356,长度是偶数,节点类型是leaf,所以可以得出HP前缀是0x20,HP编码是0x206f727356。所以可得该leaf节点:(0x206f727356,"stallion")。

所以当前默克尔树如下图:

4.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

说完了8,我们再说说4这部分,路径4后面有共同路径6f,6f后才产生null和6两条分叉。

共同路径6f是一个extension节点,extension节点格式不再介绍,开始计算HP编码,6f长度是偶数,又是extension类型,所以HP前缀为0x00,HP编码为0x006f。

所以当前默克尔树如下图:

5.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

6f后分出了null和6,是多条路径,所以HashD的节点是一个branch节点,6是索引值,索引为6的元素存储着子节点hash;而null是没有的,上文提到:vt为根节点到当前节点的父节点所经过的路径组成的键对应的value。

则代表当前HashD节点该存储从根节点到父节点0x646f组成的键对应的值:'verb'。那么该由HashD的vt保存'verb'。

所以当前默克尔树如下图:

6.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

接下来是共同路径7,一个extension节点,开始计算HP编码,7长度是奇数,又是extension类型,所以HP前缀为0x1,HP编码为0x17。

所以当前默克尔树如下图:

7.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

7后分出了null和6,是多条路径,与第五步相同,HashF是一个branch节点,索引为6的元素存储子节点哈希,vt存储'puppy'的值

所以当前默克尔树如下图:

8.

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

好了,现在只剩下一条路径了,表示这最后一个是一个leaf叶子节点,路径为5,路径长度为奇数,索引HP前缀为0x3,HP编码为0x35

所以当前也是最终的默克尔树如下图:

以上就是这棵默克尔树构造的全部过程了,写得很详细,是希望能够为在阅读书籍时没有理顺思路的小伙伴们提供一些帮助。

三、总结

从构造过程中我们可以看出,MPT中节点之间,是通过哈希值来确定的。由于哈希值的特性,只要数据有了微小改动,就会导致根节点改变,所以我们可以用树的根节点来代表整个树中数据的状态,这样就不用保存整个树的数据。

在以太坊中,默克尔树有着大量的应用,比如保存和验证系统中的所有账户状态、所有合约的存储状态、区块中的所有交易和所有收据数据的状态等。

参考

【深度知识】以太坊区块数据结构及以太坊的4棵数

《深入以太坊智能合约开发》

转载自:https://learnblockchain.cn/article/5321