【Solidity】安全与校验

信息传输

发送方 A:

  1. 计算消息 message 的哈希值 H:hash(message) = H

  2. 私钥 privateKey ➕ 哈希值 H 🟰 签名 signature:signature = sign(H, privateKey)

  3. 将消息 message 和签名 signature 发送给 B

接收方 B:

  1. 计算消息 message 的哈希值 H1:hash(message) = H1

  2. 公钥 publicKey ➕ 签名 signature 🟰 H2:H2 = verify(signature, publicKey)

  3. 比较 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 的长度信息, 从 32 字节开始才是真正的签名数据
            r := mload(add(_signature, 32))
            // 从 _signature 的第 64 字节开始加载 32 字节的数据, 并将其赋值给 s
            s := mload(add(_signature, 64))
            // 从 _signature 的第 96 字节开始加载 32 字节的数据, 并取其第 1 个字节给 v
            v := byte(0, mload(add(_signature, 96)))
            // 在以太坊中, 签名通过椭圆曲线数字签名算法 (ECDSA, Elliptic Curve Digital Signature Algorithm) 创建
            // 其中 r s 是签名的一部分, v 是签名的恢复因子 (为 27 / 28), 用于恢复签名者的公钥
        }
    }

    // 计算给定消息的哈希值
    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:

  1. 将要发送的消息作为参数传入 getMessageHash 方法,得到消息的哈希值 H,这里以字符串 “hello” 为例

  2. F12 打开控制台,执行 ethereum.enable();查看 Promise,若为 fulfilled 状态则说明 MetaMask 已连接,可查看 Promise 结果得到 MetaMask 的账号地址(需要先安装 MetaMask 插件并登录)

  3. 执行 ethereum.request({ method: "personal_sign", params: [步骤 2 得到的 MetaMask 账号地址, 步骤 1 得到的哈希值 H] }),会弹出签名框,点击 sign 进行签名;查看 Promise,若为 fulfilled 状态则说明签名成功,可查看 Promise 结果得到签名 signature

  4. 假设 A 将步骤 3 得到的签名 signature 和消息 “hello” 发送给了 B

模拟接收方 B:

  1. 将收到的消息 “hello” 作为参数传入 getMessageHash 方法,得到消息的哈希值

  2. 将步骤 1 得到的哈希值作为参数传入 getEthSignedMessageHash 方法,得到符合以太坊签名标准的消息哈希值 H1

  3. 将步骤 2 得到的消息哈希值和收到的签名 signature 作为参数传入 recover 方法,得到签名者的地址;比对是否为 A 步骤 2 得到的 MetaMask 账号地址,如果不一致 则说明信息被篡改 / 消息不是来自 A

步骤 1 2 3 即为 verify 方法的逻辑,可直接调用 verify 方法进行验证



访问控制

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);
    }
}
  1. 部署合约,部署者将成为管理员

  2. 获取编辑器地址和 ROLE_ADMIN 的哈希值,填入 roles 中查看权限,此处应为 true

  3. 更新编辑器地址,获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 false

  4. 传入新的编辑器地址,使用管理员地址调用 grantUserRole 方法,授权新的编辑器地址为 ROLE_USER

  5. 获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 true

  6. 传入新的编辑器地址,使用管理员地址调用 revokeUserRole 方法,取消新的编辑器地址的 ROLE_USER 权限

  7. 获取新的编辑器地址和 ROLE_USER 的哈希值,填入 roles 中查看权限,此处应为 false

  8. 不使用管理员地址调用 grantUserRole 方法,会报错



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JS.Huang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值