您正在查看: 2023年2月

如何在solidity中开始使用无gas元交易

元交易,也被称为 "无gas" 交易,是一种允许用户与智能合约交互而无需自己支付gas的方式。这对于需要用户进行频繁或小额交易的应用来说特别有用,因为Gas费的成本会迅速增加。在这篇博文中,我们将讨论如何在Solidity中开始实现无gas元交易,Solidity是用于在以太坊区块链上编写智能合约的编程语言。

什么是元交易?

在以太坊网络中,每次用户想与智能合约交互时,他们必须向网络支付一笔费用(以Gas形式),以便执行交易。这种费用对于激励矿工将交易纳入区块链并确保网络保持去中心化和安全是必要的。

然而,这种模式对于需要用户进行频繁或小额交易的应用来说是有局限性的,因为Gas费用的成本会迅速增加,并成为用户进入的障碍。元交易提供了一种解决方法,允许用户与智能合约交互,而不必自己支付加Gas费。

在元交易中,用户的交易实际上是由另一个账户执行的,该账户代表他们支付Gas费。这个账户被称为 "relayer",它可以是一个合约或普通的以太坊账户。中继者从用户那里收到交易,用自己的私钥签名,然后将其提交给网络进行开采。用户的交易基本上被包裹在支付Gas费用的第二笔交易中,允许用户与合约交互,而无需自己支付Gas费用。

在 Solidity 中实现无gas元交易

为了在Solidity智能合约中实现无gas元交易,我们需要做以下工作。

  1. 创建一个函数,允许中继者代表用户执行交易。
  2. 检查中继者是否被授权代表用户执行交易。
  3. 验证用户交易的签名以确保其真实性。
  4. 执行用户的交易,并使用中继者的账户支付Gas费。

让我们更详细地了解一下这些步骤中的每一个。

1. 为中继者创建一个函数来执行交易

首先,我们需要在我们的智能合约中创建一个函数,允许中继者代表用户执行交易。这个函数应该接受以下参数。

  • _user: 想执行交易的用户的地址。
  • _data: 用户的交易数据,编码为字节数组。这通常是用户想调用的函数的签名和参数,使用abi.encode()函数进行编码。
  • _signature: 用户交易的签名,使用eth_signTypedData()函数生成。

下面是这个函数在Solidity中的一个例子。

function execute(address _user, bytes _data, bytes _signature) public {
  // TODO: Add code to verify the relayer and signature
  // TODO: Add code to execute the user's transaction
}

2. 检查中继者是否被授权

接下来,我们需要检查中继器是否被授权代表用户执行交易。这对于防止恶意行为者代表其他用户提交任意交易非常重要。

做到这一点的一个方法是让用户明确授权中继器代表他们执行交易。这可以通过在智能合约中添加一个映射来实现,该映射存储了每个用户的授权中继者。然后execute()函数可以检查这个映射,以验证调用者是否被授权代表用户执行交易。

下面是一个例子,说明这种映射和验证在Solidity中可能是怎样的。

mapping(address => address[]) public authorizedRelayers;

function execute(address _user, bytes _data, bytes _signature) public {
  // Check that the caller is authorized to execute transactions on behalf of the user
  require(authorizedRelayers[_user].contains(msg.sender), "Unauthorized relayer");

  // TODO: Add code to verify the signature
  // TODO: Add code to execute the user's transaction
}

在这个例子中,authorizedRelayers映射被用来为每个用户存储一个授权中继者数组。然后execute()函数检查调用者(msg.sender)是否在该用户的授权中继者数组中,然后再继续执行。

3. 验证签名

接下来,我们需要验证用户交易的签名,以确保它是真实的。这对于防止恶意行为者提交实际上并非由用户签名的交易非常重要。

为了验证签名,我们可以使用ecrecover()函数,该函数将签名、交易数据和链ID作为输入,并返回签署该交易的地址。然后我们可以将这个地址与传递给execute()函数的_user参数进行比较,以确保它们相匹配。

下面是这个签名验证在Solidity中可能出现的例子。

function execute(address _user, bytes _data, bytes _signature) public {
  // Check that the caller is authorized to execute transactions on behalf of the user
  require(authorizedRelayers[_user].contains(msg.sender), "Unauthorized relayer");

  // Verify the signature
  bytes32 hash = keccak256(abi.encodePacked(chainId, _data));
  address signer = ecrecover(hash, sig.v, sig.r, sig.s);
  require(signer == _user, "Invalid signature");

  // TODO: Add code to execute the user's transaction
}

4. 执行用户的交易

最后,我们需要执行用户的交易,用中继者的账户支付Gas费。要做到这一点,我们可以使用delegatecall()函数,它允许我们用当前合约的调用者和参数调用另一个合约的函数。

下面是一个在Solidity中可能出现的例子。

function execute(address _user, bytes _data, bytes _signature) public {
  // Check that the caller is authorized to execute transactions on behalf of the user
  require(authorizedRelayers[_user].contains(msg.sender), "Unauthorized relayer");

  // Verify the signature
  bytes32 hash = keccak256(abi.encodePacked(chainId, _data));
  address signer = ecrecover(hash, sig.v, sig.r, sig.s);
  require(signer == _user, "Invalid signature");

  // Execute the user's transaction
  // The relayer's account is used to pay for the gas fees
  delegatecall(_data);
}

在这个例子中,delegatecall()函数被用来执行用户的交易,使用relayer的账户来支付Gas费。_data参数包含用户交易的函数签名和参数,被传递给delegatecall()函数作为调用的合约和参数。

总结

在这篇博文中,我们讨论了如何在Solidity中实现无gas元交易,Solidity是用于在以太坊区块链上编写智能合约的编程语言。我们走过了以下步骤:创建一个允许中转者代表用户执行交易的函数,验证中转者是否被授权,签名是否真实,以及执行用户的交易,同时使用中转者的账户支付Gas费用。

元交易对于需要用户进行频繁或小额交易的应用来说是一个有用的工具,因为它们允许用户与智能合约交互,而不必自己支付Gas费。通过遵循这篇博文中概述的步骤,你可以在你自己的 Solidity 智能合约中实现无gas元交易。
Source:https://coinsbench.com/how-to-get-started-with-gasless-meta-transactions-in-solidity-90e6d18f758
转载自:https://learnblockchain.cn/article/5347

EIP2612: 通过链下签名授权实现更少 Gas 的 ERC20代币

解锁消耗到了大量的 gas

每个人都在谈论 “无gas” 的以太坊交易,因为没有人喜欢支付gas费用。 但是以太坊网络的运行正是因为交易是付费的。 那么,你怎么才能“无gas”交易呢? 这是什么法术?
在本文中,我将展示如何使用 “无 gas” 交易背后的模式。 你会发现,尽管以太坊没有免费的午餐之类的东西,但是你可以通过有趣的方式改变 gas 成本。
通过运用本文中的知识,你的用户将节省大量 gas,享受更好的用户体验,甚至可以在你的智能合约中构建新颖的委派模式。
可是等等! 还有更多! 为方便起见,我将所需的所有工具都放在了此存储库中。 因此,现在你实现 “无 gas” 代币的障碍就突然降低了很多。
让我们开始吧。

背景

我不得不承认,即使我知道如何在智能合约中实现“无 gas”交易,但对于使它们成为可能的密码学我也知之甚少。 那对我来说不是障碍,所以对你也不应该是。

据我所知,私钥用于签署发送给以太坊的交易,一些密码学魔术用于将我(签名者)识别为msg.sender。 这支撑了以太坊中所有访问控制。

“无 gas” 交易背后的法宝是,我可以使用我的私钥和要执行的智能合约交易进行签名。

签名是在链下进行的,而无需花费任何 gas。 然后,我可以将此签名交给其他人,以他们的名义代表我执行交易。

签名函数通常就是常规合约方法,但会使用其他签名参数进行扩展。 例如,在dai.sol中,我们有授权(approve)函数:

function approve(address usr, uint wad) external returns (bool)

我们还具有permit许可函数,该功能与approve函数相同,但是将签名作为参数。

function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external

不用担心所有这些额外的参数,我们将介绍它们。 你需要注意的是这两个函数都使用allowance映射执行的操作:

function approve(address usr, uint wad) external returns (bool)
{
  allowance[msg.sender][usr] = wad;
  …
}

function permit(
  address holder, address spender,
  uint256 nonce, uint256 expiry, bool allowed,
  uint8 v, bytes32 r, bytes32 s
) external {
  …
  allowance[holder][spender] = wad;
  …
}

如果使用approve,则允许spender最多使用wad个代币。

如果你给某人提供有效的签名,则该人可以调用permit以允许spender 使用你的代币。

因此,基本上,“无 gas”交易背后的模式是制作可以提供给某人的签名,以便他们可以安全地执行特殊交易。 这就像授予某人执行函数的权限。

这是一种授权模式。

标准

如果你像我一样,那么你要做的第一件事就是深入研究代码。 我立即注意到此注释:

// — — EIP712 niceties — -

有了这个,我钻进了兔子洞,却无望地迷路了。 现在,我已经理解了,我可以用简单的方式来解释它。

EIP712描述了如何以通用方式构建函数签名。 其他EIP描述了如何将EIP712应用。 例如,EIP2612描述了如何使用EIP712的签名应用于permit函数,其功能应与ERC20代币中的approve功能相同。

如果你只想实现之前提到的签名功能,例如将签名批准添加到自己的MetaCoin,则可以阅读EIP2612 ,你甚至可以继承实现过的合约,并减轻生活压力。

在本文中,我们将研究dai.sol中“无 gas”交易的实现。 这将使事情变得清晰。 dai.sol实现发生在EIP2612之前,会略有不同。 那不会有问题。

签名组成

EIP712签名的早期实现可以在dai.sol源码中找到 。 它允许Dai持有人通过计算链下签名并将其提供给支出者(spender)来批准转账交易,而不是自己调用approve函数。
它包含下面几个部分:

  1. 一个 DOMAIN_SEPARATOR .
  2. 一个 PERMIT_TYPEHASH .
  3. 一个 nonces 变量.
  4. 一个 permit 函数.

这是DOMAIN_SEPARATOR,和相关变量:

string  public constant name     = "Dai Stablecoin";
string  public constant version  = "1";
bytes32 public DOMAIN_SEPARATOR;
constructor(uint256 chainId_) public {
  ...
  DOMAIN_SEPARATOR = keccak256(abi.encode(
    keccak256(
      "EIP712Domain(string name,string version," + 
      "uint256 chainId,address verifyingContract)"
    ),
    keccak256(bytes(name)),
    keccak256(bytes(version)),
    chainId_,
    address(this)
  ));
}

DOMAIN_SEPARATOR只不过是唯一标识智能合约的哈希。 它是由EIP712域(EIP712Domain)的字符串,包含代币合约的名称,版本,所在的chainId以及合约部署的地址构成。

所有这些信息都在构造函数上进行hash 运算赋值到DOMAIN_SEPARATOR变量中,该变量在创建线下签名时由持有人使用,并且在执行permit时需要匹配。 这样可以确保签名仅对一个合约有效。

这是PERMIT_TYPEHASH:

PERMIT_TYPEHASH 是函数名称(大写开头)和所有参数(包括类型和名称)的哈希。 目的是清楚地标志签名的函数。

签名将在permit函数中处理,如果使用的PERMIT_TYPEHASH不是该特定函数的签名,它将回退交易。 这样可以确保仅将签名用于预期的功能。

然后是nonces映射:

mapping (address => uint) public nonces;

该映射记录了特定持有人已使用了多少次签名。 创建签名时,需要包含一个nonces值。 执行permit时,所包含的nonce 值必须与该持有人到目前为止使用的签名数完全匹配。 这样可以确保每个签名仅使用一次。

所有这三个条件,即PERMIT_TYPEHASH,DOMAIN_SEPARATOR和nonce,确保每个签名仅用于预期的合约,预期的函数,并且仅使用一次。

现在,让我们看看如何在智能合约中处理签名。

permit 函数

permit是dai.sol里实现的函数,允许使用签名来修改持有人的 allowance对spender授权的数量。

// --- 通过签名授权 ---
function permit(
  address holder, address spender,
  uint256 nonce, uint256 expiry, bool allowed,
  uint8 v, bytes32 r, bytes32 s
) external;

如你所见,permit有很多参数。 它们是计算签名所需的所有参数,加上签名本身就是v, r和s。

你需要用参数创建签名似乎很愚蠢,但是你确实需要。 因为仅能从签名中恢复签名的地址。 我们将使用所有参数和恢复的地址来确保签名有效。

首先,我们使用确保安全性所需的所有参数来计算digest。 作为签名创建的一部分,holder将需要在链下计算出完全相同的digest:

bytes32 digest =
  keccak256(abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR,
    keccak256(abi.encode(
      PERMIT_TYPEHASH,
      holder,
      spender,
      nonce,
      expiry,
      allowed
    ))
  ));

使用ecrecover和v,r,s签名,我们可以恢复地址。 如果它是holder的地址,我们知道所有参数都匹配DOMAIN_SEPARATOR,PERMIT_TYPEHASH,nonce,holder,spender,expiry和allowed。 哪怕是任何一点内容没匹配,则签名被拒绝:

require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");

请注意这里。 签名中有许多参数,其中一些参数有点模糊,例如chainId (它是 DOMAIN_SEPARATOR 的一部分)。 它们中的任何一个不匹配都会导致签名被拒绝,并带有完全相同的错误提示,这让链下调试签名很困难。

现在我们知道 holder 批准了这个函数调用。 接下来,我们将证明签名没有被滥用。 我们检查当前时间是否在 expiry(过期)之前,这保证了仅在特定时间内许可有效。

require(expiry == 0 || now <= expiry, "Dai/permit-expired");

我们还会检查签名中的 nonce ,以便每个签名只能使用一次。

require(nonce == nonces[holder]++, "Dai/invalid-nonce");

这些检查都通过了! dai.sol使spender可以使用的holder的代币数量设置为最大值(即allowance设置为最大),并触发一个事件,仅此而已。

uint wad = allowed ? uint(-1) : 0;
allowance[holder][spender] = wad;
emit Approval(holder, spender, wad);

dai.sol合约对 allowance 使用的是二分法设置(译者注:要么是最大,要么是 0), 在代码库,有更传统的方法。

创建链下签名

创建签名也许需要通过一些实践才可以掌握它。 我们将分三步复制智能合约中permit的功能:

  1. 生成 DOMAIN_SEPARATOR
  2. 生成 digest
  3. 创建交易签名

以下函数将生成 DOMAIN_SEPARATOR。 它与dai.sol构造函数中的代码相同,但在JavaScript中实现,并使用ethers.js的keccak256,defaultAbiCoder和toUtfBytes,它需要代币名称和部署地址,以及chainId。 假定代币版本为“1”。

以下函数将为特定的permit调用生成digest。 注意,holder,spender,nonce 和 expiry作为参数传递。 为了清楚起见,它还传递了一个 approve.allowed 参数,尽管你可以将其始终设置为 true,否则签名将被拒绝。从刚刚dai.sol复制PERMIT_TYPEHASH。

一旦我们有了digest,对其进行签名就相对容易了。 我们从[digest]中删除0x前缀后,使用ethereumjs-util中的ecsign。 请注意,我们需要用户私钥才能执行此操作。

在代码中,我们将按以下方式调用这些函数:

请注意,对permit的调用需要重用用于创建digest的所有参数。 只有在这种情况下,签名才有效。

还要注意的是,此代码段中仅有的两个交易是由user2调用的。 user1是holder,是创建digest并签名的用户。 但是,user1并没有花费任何gas。

user1将签名提供给user2,后者使用它来执行user1授权的 permit 和transferFrom。

从 user1的角度来看,这是一次“无 gas”交易, 他没有花一分钱。

结论

本文介绍了如何使用“无Gas”交易,阐明了“无Gas”实际上意味着将Gas成本转移给其他人。 为此,我们需要一个智能合约中的功能,该功能可以处理预先签署的交易,并且需要进行大量的数据检验以确保一切安全。

但是,使用此模式有很多好处,因此,它被广泛使用。 签名允许将交易 gas 成本从用户转移到服务提供商,从而在许多情况下消除了相当大的障碍。 它还允许实现更高级的委派模式,通常会对UX进行相当大的改进。

已为您提供入门代码库,请使用它。
本翻译由 Cell Network 赞助支持。

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

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