信息传输
发送方 A:
- 计算消息 message 的哈希值 H:
hash(message) = H
- 私钥 privateKey ➕ 哈希值 H 🟰 签名 signature:
signature = sign(H, privateKey)
- 将消息 message 和签名 signature 发送给 B
接收方 B:
- 计算消息 message 的哈希值 H1:
hash(message) = H1
- 公钥 publicKey ➕ 签名 signature 🟰 H2:
H2 = verify(signature, publicKey)
- 比较 H1 和 H2,如果相等 则说明消息未被篡改且确实来自 A
Keccak256 哈希函数
contract HashFunc {
function hash(
string memory _testString,
uint _testUint
) public pure returns (bytes32) {
// 先对数据进行编码, 再用 keccak256 加密
return keccak256(abi.encodePacked(_testString, _testUint));
}
}
encodePacked 方法可以对多个参数进行编码,并压缩编码后的结果,节省 gas 费用。但某些情况下会导致哈希碰撞(哈希冲突)。
为了避免哈希碰撞,可以使用 encode 方法,它不会压缩编码结果,但会消耗更多 gas。
contract HashFunc {
function hash(
string memory _testString,
uint _testUint
) public pure returns (bytes32) {
// 使用 encode 方法避免哈希碰撞
return keccak256(abi.encode(_testString, _testUint));
}
}
除了使用 encode 方法,我们还可以在 encodePacked 的入参之间再插入一个参数,这样也能避免哈希碰撞。
contract HashFunc {
function hash(
string memory _string1,
uint _uint, // 用来避免哈希碰撞的参数
string memory _string2
) public pure returns (bytes32) {
// 在 encodePacked 的入参之间再插入一个参数,避免哈希碰撞
return keccak256(abi.encodePacked(_string1, _uint, _string2));
}
}
签名与验证
contract VerifySig {
// 将一个 65 字节长的签名拆分成 r、s 和 v 三个部分
function split(
bytes memory _signature
) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
require(_signature.length == 65, "invalid signature length");
assembly {
// 从 _signature 的第 32 字节开始加载 32 字节的数据, 并将其赋值给 r
// 这是因为前 32 字节是 _signature 的长度信息
r := mload(add(_signature, 32))
// 从 _signature 的第 64 字节开始加载 32 字节的数据, 并将其赋值给 s
s := mload(add(_signature, 64))
// 从 _signature 的第 96 字节开始加载 32 字节的数据, 并取其第一个字节给 v
v := byte(0, mload(add(_signature, 96)))
}
}
// 计算给定消息的哈希值
function getMessageHash(
string memory _message
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_message));
}
// 生成一个符合以太坊签名标准的消息哈希值
function getEthSignedMessageHash(
bytes32 _messageHash
) public pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
// 这是一个固定的前缀, 用于防止签名重用攻击
// 这个前缀告诉以太坊客户端这是一个签名消息, 而不是交易或其他数据
// \x19 表示消息的长度; 32 表示消息哈希的长度为 32 字节
_messageHash
)
);
}
// 从签名中恢复出签名者的地址
function recover(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = split(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
// 验证消息的有效性 (签名者是否正确、消息是否被篡改)
function verify(
address _signer,
string memory _message,
bytes memory _signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_message);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recover(ethSignedMessageHash, _signature) == _signer;
}
}
部署合约并测试:
模拟发送方 A:
- 将要发送的消息作为参数传入 getMessageHash 方法,得到消息的哈希值 H,这里以字符串 “hello” 为例
- F12 打开控制台,执行
ethereum.enable()
;查看 Promise,若为 fulfilled 状态则说明 MetaMask 已连接,可查看 Promise 结果得到 MetaMask 的账号地址(需要先安装 MetaMask 插件并登录) - 执行
ethereum.request({ method: "personal_sign", params: [步骤 2 得到的 MetaMask 账号地址, 步骤 1 得到的哈希值 H] })
,会弹出签名框,点击 sign 进行签名;查看 Promise,若为 fulfilled 状态则说明签名成功,可查看 Promise 结果得到签名 signature - 假设 A 将步骤 3 得到的签名 signature 和消息 “hello” 发送给了 B
模拟接收方 B:
- 将收到的消息 “hello” 作为参数传入 getMessageHash 方法,得到消息的哈希值
- 将步骤 1 得到的哈希值作为参数传入 getEthSignedMessageHash 方法,得到符合以太坊签名标准的消息哈希值 H1
- 将步骤 2 得到的消息哈希值和收到的签名 signature 作为参数传入 recover 方法,得到签名者的地址;比对是否为 A 步骤 2 得到的 MetaMask 账号地址,如果不一致 则说明信息被篡改 / 消息不是来自 A
访问控制
contract AccessControl {
// 定义两个角色
bytes32 public constant ROLE_ADMIN =
keccak256(abi.encodePacked("ROLE_ADMIN"));
bytes32 public constant ROLE_USER =
keccak256(abi.encodePacked("ROLE_USER"));
// 定义一个双重映射, 用于管理 "角色 - 用户 - 权限"
mapping(bytes32 => mapping(address => bool)) public roles;
// 分配权限
function _grantRole(bytes32 _role, address _account) internal {
roles[_role][_account] = true;
}
// 撤销权限
function _revokeRole(bytes32 _role, address _account) internal {
roles[_role][_account] = false;
}
// 构造函数
constructor() {
_grantRole(ROLE_ADMIN, msg.sender);
}
// 函数装饰器, 限制只有管理员才能调用
modifier onlyAdmin() {
require(
roles[ROLE_ADMIN][msg.sender],
"AccessControl: sender must be an admin to perform this action"
);
_;
}
// 分配权限 (外部使用, 只有管理员才能调用)
function grantUserRole(address _account) public onlyAdmin {
_grantRole(ROLE_USER, _account);
}
// 撤销权限 (外部使用, 只有管理员才能调用)
function revokeUserRole(address _account) public onlyAdmin {
_revokeRole(ROLE_USER, _account);
}
}
- 部署合约,部署者将成为管理员
- 获取编辑器地址和 ROLE_ADMIN 的哈希值,填入 roles 中查看权限,此处应为 true
- 更新编辑器地址,获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 false
- 使用管理员地址调用 grantUserRole 方法,并传入新的编辑器地址作为参数,授权新的编辑器地址为 ROLE_USER
- 获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 true
- 使用管理员地址调用 revokeUserRole 方法,并传入新的编辑器地址作为参数,取消新的编辑器地址的 ROLE_USER 权限
- 获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 false
- 不使用管理员地址调用 grantUserRole 方法,会报错
版权归原作者 JS.Huang 所有, 如有侵权,请联系我们删除。