探索智能合约的签名
- 原文:What is ecrecover in Solidity?
- 译文出自:登链翻译计划
- 译者:翻译小组
- 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
有没有想过Solidity中的ecrecover
命令到底是怎么回事?
这都是关于签名和密钥的...
你可能在Solidity合约中看到过ecrecover
,并想知道这到底是什么。那么你遇到了EVM 预编译 ecrecover。预编译是一些提前被编译的智能合约的通用函数,所以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正确地指出了这一点。正确的方法是在本文底部的最后一个例子中。
那么,这一切意味着什么呢?假设你熟悉公钥密码学的基本概念,这将很容易理解。
你可能知道,每当你向以太坊网络发送一笔交易时,必须用你的私钥签署这笔交易。自然也假设以太坊节点有某种方式来验证签名是正确的。
这种验证签名的功能也同样添加到了智能合约上。有了这个功能,你可以验证更多的东西,而不仅仅是交易签名本身。事实上,你可以将任何数据传递给智能合约,对其进行散列,然后根据数据验证其签名。上面的代码中的签名是v、r和s的组合。
实际上,之前也有文章讨论了如何使用它的例子。这些例子包括:
从本质上讲,你可以验证一个签名数据,而这些数据不一定来自交易签署者。
首先,我们需要决定签名的类型。虽然这对ecrecover来说这并不重要,但对签名来说,已经有几个标准可以被客户端使用以太坊密钥来签署数据:
eth_sign 是用来签署任意数据。这使得它是最强大的,最简单的(只是签署数据),但也是最危险的。这里的大问题是,你可以让用户签署一个数据,而这个实际上是交易数据。想象一下,你让用户登录到你的服务,但你让他们签署的数据实际上是一个交易,如 "发送5个ETH给攻击者"。交易毕竟只是由字节组成,人们很可能不会检查他们所签署的这串字符的实际含义。看似无害的签名,却成了窃取资金的攻击。所以一般不鼓励直接使用eth_sign。
personal_sign 后来加入来解决这个问题。该方法在任何签名数据前加上"\x19Ethereum Signed Message:\n",这意味着如果有人要签署交易数据,添加的前缀字符串会使其成为无效交易。
对于更复杂的用例,特别是在智能合约中使用时,EIP-712标准被创建。EIP-712标准随着时间的推移而有所改变,但目前MetaMask支持的最后一个版本是signTypedData_v4。或者你可以使用一个特定的库,如eip-712。EIP-712解决的主要问题是确保用户清楚地知道他们在签署什么,为哪个合约地址和网络签署,而且每个签名最多只能使用一次。简而言之,这是通过签署所有需要的配置数据(地址、chain id、版本、数据类型)的哈希值+实际数据本身来实现的。ERC20-Permit 是一个关于如何使用signTypedData_v4的好例子。
所有的函数都可以在与MetaMask交互时使用,见例子。另外,它们也可以使用eth-sig-util。
所以回到问题我应该使用哪种签名标准?从合约的角度来看,使用最新的EIP-712标准!eth_sign并不安全,personal_sign主要用于实现用户登录功能。在你的合同中坚持使用EIP-712。
现在让我们看看如何在Solidity中实现EIP-712。大概的想法是:
我个人还建议增加一个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有几个问题,在上面的代码中没有说明,但你应该注意:
在实践中,我再次建议使用Openzeppelin合约。他们的ECDSA实现解决了所有这三个问题,而且他们还有一个EIP-712实现(在我看来还是一个草案,但可以使用)。这不仅更容易使用,而且他们还做了进一步的改进:
上面的代码将被简化为:
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-712的第四版标准。如果你在其他合约中遇到EIP-712的实现,要注意使用的是哪个版本。
另外,最后也说明一下,调试无效的签名是非常痛苦的,因为任何数值的微小差异都会导致无效的签名,但你不知道哪些数据可能是错误的。因此,如果你遇到无效签名,一定要仔细检查你的所有输入。
另一个有趣的标准是EIP-1271。由于以太坊的智能合约背后没有私钥,所以它们不能创建那些v、r、s签名。但有了这个标准,仍然可以让合约本身创建签名,见我之前的文章的底部。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!