• 译文出自:登链翻译计划 [1]

  • 译者:翻译小组 [2]

  • 校对:Tiny 熊 [3]

有没有想过 Solidity 中的 ecrecover 命令到底是怎么回事?

这都是关于签名和密钥的 ...

Solidity 中的 ecrecover 的应用 Public Key XKCD

什么是 ecrecover ?

你可能在 Solidity 合约中看到过 ecrecover ,并想知道这到底是什么。那么你遇到了 EVM 预编译 [4] ecrecover[5]。预编译是一些提前被编译的智能合约的通用函数,所以 Ethereum 节点可以有效地运行这个函数。从合约的角度来看,这只是一个像操作码一样的单一命令。

看看下面的代码:

    function recoverSignerFromSignature(uint8 v, bytes32 r, bytes32 s, bytes32 hash) external {          address signer = ecrecover(hash, v, r, s);          require(signer != address(0), "ECDSA: invalid signature");      }  

基本上,大家就是这样使用它,尽管还有更多的内容。 不要在生产中实际使用上述代码 ,Patricio Palladino[6] 正确地指出了这一点。正确的方法是在本文底部的最后一个例子中。

Solidity 中的 ecrecover 的应用 Dont Understand Meme

那么,这一切意味着什么呢?假设你熟悉公钥密码学 [7] 的基本概念,这将很容易理解。

你可能知道,每当你向以太坊网络发送一笔交易时,必须用你的私钥签署这笔交易。自然也假设以太坊节点有某种方式来验证签名是正确的。

这种验证签名的功能也同样添加到了智能合约上。有了这个功能,你可以验证更多的东西,而不仅仅是交易签名本身。事实上,你可以将任何数据传递给智能合约,对其进行散列,然后根据数据验证其签名。上面的代码中的签名是 v、r 和 s 的组合。

为什么我需要这个?

实际上,之前也有文章讨论了如何使用它的例子。这些例子包括:

  • Meta Transactions[8]

  • 无需 gas 代币和 ERC20-Permit 还任重而道远 [9]

从本质上讲,你可以验证一个签名数据,而这些数据不一定来自交易签署者。

我应该使用哪个签名标准?

Solidity 中的 ecrecover 的应用 Standards Meme

首先,我们需要决定签名的类型。虽然这对 ecrecover 来说这并不重要,但对签名来说,已经有几个标准可以被客户端使用以太坊密钥来签署数据:

  • eth_sign

  • personal_sign

  • EIP-712

eth_sign 是用来签署任意数据。这使得它是最强大的,最简单的(只是签署数据),但也是最危险的。这里的大问题是,你可以让用户签署一个数据,而这个实际上是交易数据。想象一下,你让用户登录到你的服务,但你让他们签署的数据实际上是一个交易,如 " 发送 5 个 ETH 给攻击者 "。交易毕竟只是由字节组成,人们很可能不会检查他们所签署的这串字符的实际含义。看似无害的签名,却成了窃取资金的攻击。所以一般不鼓励直接使用 eth_sign。

personal_sign 后来加入来解决这个问题。该方法在任何签名数据前加上 "\x19Ethereum Signed Message:\n",这意味着如果有人要签署交易数据,添加的前缀字符串会使其成为无效交易。

对于更复杂的用例,特别是在智能合约中使用时,EIP-712 标准被创建。EIP-712 标准随着时间的推移而有所改变,但目前 MetaMask 支持的最后一个版本是 signTypedData_v4 。或者你可以使用一个特定的库,如 eip-712[10]。EIP-712 解决的主要问题是确保用户清楚地知道他们在签署什么,为哪个合约地址和网络签署,而且每个签名最多只能使用一次。简而言之,这是通过签署所有需要的配置数据(地址、chain id、版本、数据类型)的哈希值+实际数据本身来实现的。ERC20-Permit[11] 是一个关于如何使用 signTypedData_v4 的好例子。

所有的函数都可以在与 MetaMask 交互时使用,见例子 [12]。另外,它们也可以使用 eth-sig-util[13]。

所以回到问题 我应该使用哪种签名标准 ?从合约的角度来看,使用最新的 EIP-712 标准!eth_sign 并不安全,personal_sign 主要用于实现用户登录功能。在你的合同中坚持使用 EIP-712。

如何实现 EIP-712

现在让我们看看如何在 Solidity 中实现 EIP-712。大概的想法是:

  1. 计算一个域的哈希值,该值涵盖了合约地址和 chainId 的配置数据

  2. 计算结构化的数据哈希值

  3. 结合这两个哈希值,并在 ecrecover 中使用它。

我个人还建议增加一个 nonce 和 deadline 值,以防止重放攻击并确保在特定时间内执行。这些不是 EIP-712 标准的直接组成部分,但可以很容易地添加。下面你会发现一个例子,如何实现这些,然后加上合约的本身的参数去执行它:

    function executeMyFunctionFromSignature(          uint8 v,          bytes32 r,          bytes32 s,          address owner,          uint256 myParam,          uint256 deadline      ) external {          bytes32 eip712DomainHash = keccak256(              abi.encode(                  keccak256(                      "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"                  ),                  keccak256(bytes("MyContractName")),                  keccak256(bytes("1")),                  block.chainid,                  address(this)              )          );          bytes32 hashStruct = keccak256(              abi.encode(                  keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),                  owner,                  myParam,                  nonces[owner],                  deadline              )          );          bytes32 hash = keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, hashStruct));          address signer = ecrecover(hash, v, r, s);          require(signer == owner, "MyFunction: invalid signature");          require(signer != address(0), "ECDSA: invalid signature");          require(block.timestamp < deadline, "MyFunction: signed transaction expired");          nonces[owner]++;         _myFunction(owner, myParam);      }  

ecrecover 的安全问题+解决方案

ecrecover 有几个问题,在上面的代码中没有说明,但你应该注意:

  1. 在某些情况下,ecrecover 可以返回一个随机地址,而不是无效签名的 0。这一点可以通过结构化数据中加入所有者地址来防止。

  2. 签名是可塑的 [14],这意味着你可能会为同一数据创建第二个同样有效的签名。在我们的案例中,我们没有使用签名数据本身(例如,可以作为一个 ID)。

  3. 如果哈希值不是在合约本身中计算的,攻击者可以构建一个看起来有效的哈希值和签名。

在实践中,我再次建议使用 Openzeppelin 合约。他们的 ECDSA 实现解决了所有这三个问题,而且他们还有一个 EIP-712 实现(在我看来还是一个草案,但可以使用)。这不仅更容易使用,而且他们还做了进一步的改进 :

  • eip712DomainHash 的缓存机制,所以只有在 chainId 改变时才会计算(所以通常只计算一次)

  • 如上所述,对签名的额外安全检查

  • 能够以字符串形式发送签名

上面的代码将被简化为 :

    import "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";      import "@openzeppelin-contracts/contracts/utils/cryptography/draft-EIP712.sol";      contract MyContract is EIP712 {          function executeMyFunctionFromSignature(              bytes memory signature,              address owner,              uint256 myParam,              uint256 deadline          ) external {              bytes32 digest =_hashTypedDataV4(keccak256(abi.encode(                  keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),                  owner,                  myParam,                  nonces[owner],                  deadline              )));              address signer = ECDSA.recover(digest, signature);              require(signer == owner, "MyFunction: invalid signature");              require(signer != address(0), "ECDSA: invalid signature");              require(block.timestamp < deadline, "MyFunction: signed transaction expired");              nonces[owner]++;             _myFunction(owner, myParam);          }      }  

这就是目前最新的 EIP-721 的第四版标准。如果你在其他合约中遇到 EIP-712 的实现,要注意使用的是哪个版本。

另外,最后也说明一下,调试无效的签名是非常痛苦的,因为任何数值的微小差异都会导致无效的签名,但你不知道哪些数据可能是错误的。因此,如果你遇到无效签名,一定要仔细检查你的所有输入。

另一个有趣的标准是 EIP-1271[15]。由于以太坊的智能合约背后没有私钥,所以它们不能创建那些 v、r、s 签名。但有了这个标准,仍然可以让合约本身创建签名,见我之前的文章 [16] 的底部。

来源:What is ecrecover in Solidity? [17]

参考资料

[1]

登链翻译计划 : https://github.com/lbc-team/Pioneer

[2]

翻译小组 : https://learnblockchain.cn/people/412

[3]

Tiny 熊 : https://learnblockchain.cn/people/15

[4]

EVM 预编译 : https://ethervm.io/#3F

[5]

ecrecover: https://docs.klaytn.com/smart-contract/precompiled-contracts#address-0x-01-ecrecover-hash-v-r-s

[6]

Patricio Palladino: https://twitter.com/alcuadrado

[7]

公钥密码学 : https://en.wikipedia.org/wiki/Public-key_cryptography

[8]

Meta Transactions: https://soliditydeveloper.com/meta-transactions

[9]

无需 gas 代币和 ERC20-Permit 还任重而道远 : https://learnblockchain.cn/article/1790

[10]

eip-712: https://github.com/Mrtenz/eip-712

[11]

ERC20-Permit: https://learnblockchain.cn/article/1790

[12]

例子 : https://github.com/danfinlay/js-eth-personal-sign-examples

[13]

eth-sig-util: https://github.com/MetaMask/eth-sig-util

[14]

可塑的 : http://coders-errand.com/malleability-ecdsa-signatures/

[15]

EIP-1271: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md

[16]

之前的文章 : https://soliditydeveloper.com/meta-transactions

[17]

What is ecrecover in Solidity? : https://soliditydeveloper.com/ecrecover

Solidity 中的 ecrecover 的应用