欢迎订阅专栏:3分钟Solidity--智能合约--Web3区块链技术必学
如需获取本内容的最新版本,请参见 Cyfrin.io 上的“验证签名(代码示例)”
消息可以在链下签名,然后通过智能合约在链上进行验证。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/*签名验证
如何签名与验证
# 签名步骤
1. 创建待签名的消息
2. 对消息进行哈希处理
3. 签署哈希值(链下操作,务必保管好私钥)
# 验证步骤
1. 根据原始消息重新生成哈希值
2. 通过签名和哈希值还原签名者
3. 将还原的签名者与声明签名者进行比对
*/
contract VerifySignature {
/* 1. 解锁MetaMask账户
ethereum.enable()
*/
/* 2. 获取消息哈希以进行签名
getMessageHash(
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
123,
"coffee and donuts",
1
)
hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
*/
function getMessageHash(
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}
/* 3. 签名消息哈希
# 使用浏览器
account = "在此处复制并粘贴签名人账户"
ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
# 使用 web3
web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
不同账户的签名会有所不同
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
/*
签名是通过对以下格式的keccak256哈希进行签名生成的:
"\x19Ethereum Signed Message\n" + len(msg) + msg
*/
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
/* 4. 验证签名
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (bytes32 r, bytes32 s, uint8 v)
{
require(sig.length == 65, "invalid signature length");
assembly {
/*
前32个字节存储签名的长度
add(sig, 32) = pointer of sig + 32
实际上,跳过了签名的前32个字节
mload(p) 从内存地址 p 开始将接下来的 32 字节加载到内存中
*/
// 前32字节,在长度前缀之后
r := mload(add(sig, 32))
// 第二个 32 字节
s := mload(add(sig, 64))
// 最终字节(接下来的32个字节的第一个字节)
v := byte(0, mload(add(sig, 96)))
}
// 隐式返回 (r, s, v)
}
}
在Remix中使用验证签名功能
重要提示:使用Remix测试签名验证时,请注意Remix处理消息哈希的方式与合约不同。Remix在创建ETH签名消息哈希之前会再次对消息哈希进行哈希运算,而合约则直接使用消息哈希。这意味着Remix和合约最终生成的ETH签名消息哈希会有所不同:
// 相同输入参数的示例: Message hash: 0x56f00a5093efc595178316938b3e9ab51b37610ca57b1b471aa4ce801f05251d Remix output: 0xd3445702e9995d1b351adf2606d88910d12dd95554f0bbdaa8d02061933c6363 Contract output: 0xed08430382ce60ae9e2b032b99a36b2c5c5c5a3fa1d293926ce87c723f2fce84为了正确测试,您可能需要改用ethers.js或web3.js,如上例链接所示。
-
获取消息哈希值:
- 使用以下示例参数调用
getMessageHash:
- 使用以下示例参数调用
address _to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 // which is the second account on Remix
uint256 _amount: 123
string _message: "coffee and donuts"
uint256 _nonce: 1
- 复制返回的messageHash
-
签名消息哈希:
-
在“部署和运行交易”选项卡中(您应该已经在此处)
-
选择第一个账户
-
点击
+号后的图标来签署消息,它将弹出一个窗口 -
粘贴messageHash
-
点击“签名”
-
Remix 返回:
- 哈希值 (ethSignedMessageHash)
- 签名 (验证时需要用到)
-
-
验证签名:
- 使用以下参数调用
verify:
- 使用以下参数调用
address _signer: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
address _to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
uint256 _amount: 123
string _message: "coffee and donuts"
uint256 _nonce: 1
bytes signature: [Signature from step 2]
- 如果签名正确,应返回
true
Remix Lite 尝试一下
END