消息签名与Permit

参考:消息签名与Permit – exchen's blog

消息签名与Permit

ECR20 标准代币有一个 approve 函数,它可以将当前用户指定的币数授权给另一个账户操作,该函数定义如下:

1

2

function approve(address spender, uint256 amount) public returns (bool)

调用 approve 函数必须是这个账户的 Owner 才可以执行操作,有没有一种办法像比特币那种使用账户的私钥离线签名,验证签名后就能执行操作,不需要直接使用 Owner 联网操作? permit 可以做到,这个函数是 ERC20 的扩展,定义如下:

1

2

function permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s)

在学习 permit 原理之前,有必要先了解 Solidity 如何打包与哈希运算、如何离线签名、如何验签等一些知识点。

打包与哈希运算

Solidity 一般是调用 keccak256 函数计算哈希。我们做一个测试,定义一个 getHash 的函数,接受一个字符串,将字符串使用 encodePacked 打包,然后再调 keccak256 即可获取字符串的哈希,代码如下:

1

2

3

4

    function getHash(string memory message) external pure returns(bytes32){

        return keccak256(abi.encodePacked(message));

    }

encodePacked 是一个很有用的函数,它能够将任一数据,打包成十六进制数据,方便做进一步处理,比如将三个不同类型的变量数据加包在一起,代码如下:

1

2

3

4

function encodePacked(string memory message, address addr, uint num) external pure returns (bytes memory){

        return abi.encodePacked(message, addr, num);

}

执行之后,在 getHash 函数输入参数字符串 123,会看到返回的 Hash 值,在 encodePacked 函数里输入三个参数,分别是字符串 123、地址 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,数字 1,会看到打包后的结果,如下图所示:

消息签名与验签

编写一个合约用于验签,名称为 VerifySign,有 5 个函数,第一个函数名称是 getMessageHash,功能接受一个字符串参数获取哈希,在上面我们讲解了哈希的获取方法。第二个函数名称是 getEthSignedMessageHash,传入的参数是第一个函数获取到的哈希,这个函数的功能相当于获取了两次哈希。第三个函数名称是 getSigner,功能是获取签名者,返回签名者的地址,对比这个地址是否匹配即可验签。第四个函数名称是 splitSign,用于拆分签名数据,将签名数据拆分成 r,s,v 三个数据,这三个数据可以了解一下椭圆加密。第五个函数名称是 verify,就是判断签名者的地址是否与我们提供的地址一样,一样则代表验签成功,否则验签失败。具体代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

pragma solidity ^0.8.0;

contract VerifySign {

    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", _messageHash));

    }

    function getSigner(bytes32 _ethSignedMessageHash, bytes memory _sign) public pure returns(address){

        (bytes32 r, bytes32 s, uint8 v) = splitSign(_sign);

        return ecrecover(_ethSignedMessageHash, v, r, s); //返回签名者地址

    }

    function splitSign(bytes memory _sign) public pure returns (bytes32 r, bytes32 s, uint8 v){

        assembly {

            r := mload(add(_sign, 32))

            s := mload(add(_sign, 64))

            v := byte(0, mload(add(_sign, 96)))

        }

    }

    function verify(address _signer, string memory _message, bytes memory _sign) external pure returns (bool){

        bytes32 messageHash = getMessageHash(_message);

        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        address signer = getSigner(ethSignedMessageHash, _sign);

        if(signer == _signer){

            return true;

        }

        return false;

    }

}

验签的合约写好了,如何签名呢?在 geth 客户端里提供了签名的方法,加载 geth,先查看当前的账户,如果没有账户则新建

1

2

3

4

5

6

7

8

9

10

> eth.accounts

[]

> personal.newAccount();

Passphrase:

Repeat passphrase:

"0x1f0dd411dca792d3cc502515a325755315365b52"

> eth.accounts

["0x1f0dd411dca792d3cc502515a325755315365b52"]

新建好账户后,需要先输入密码解锁账户

1

2

3

4

5

personal.unlockAccount(eth.accounts[0])

Unlock account 0x1f0dd411dca792d3cc502515a325755315365b52

Passphrase:

true

接下来布署合约,输入字符串 hello,记录返回的哈希数据

在 geth 客户端调用 web3 接口,指定 0x1f0dd411dca792d3cc502515a325755315365b52 账户给 hello 的哈希数据签名,返回结果如下:

1

2

3

4

5

> web3.personal.sign("0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8", "0x1f0dd411dca792d3cc502515a325755315365b52")

Give password for account 0x1f0dd411dca792d3cc502515a325755315365b52

Password:

"0x252561ffdd60f69b7b62b136dbb588c32af5b131804b22814da0c1acd78f4a655d206bfb1e8632d8e792ddc360ba1d7edb23e2e446cc24a7661b7c1d5a38ef9c1b"

将 geth 客户端返回的签名数据放入 getSigner 的参数 _sign,_ethSignedMessageHash 参数是对 messageHash 的第二参哈希,返回的结果是 0x1F0dD411dca792d3CC502515a325755315365B52,与 geth 客户端返的签名账户一致,说明验签成功。

![image-20220803164949764](/Users/geek/Library/Application Support/typora-user-images/image-20220803164949764.png)

也可以在 verify 方法里验证,提供相应的参数,返回 true 代表验签成功,如下图所示:

![image-20220803172312358](/Users/geek/Library/Application Support/typora-user-images/image-20220803172312358.png)

Permit

下面我们来看一下 permit 函数的实现,该函数所有文件是 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/draft-ERC20Permit.sol,有 6 个参数,第一个参数 owner 是被授权的地址,第二个参数 spender 是授权给谁,第三个参数 value 是授权币的数量,第四个参数 deadline 是一个时间戳,超过这个时间就无效,最后三个参数是拆分后的签名数据。其中 recover 这个函数是用于验签,返回 signer,将 signer 与 owner 做对比,一致则代表验签成功,不一致则验签失败,具体代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

function permit(

        address owner,

        address spender,

        uint256 value,

        uint256 deadline,

        uint8 v,

        bytes32 r,

        bytes32 s

    ) public virtual override {

        require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); //判断时间

        bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

        bytes32 hash = _hashTypedDataV4(structHash);

        address signer = ECDSA.recover(hash, v, r, s); //验签

        require(signer == owner, "ERC20Permit: invalid signature");

        _approve(owner, spender, value);

    }

我们需要找一个币的合约代码来做测试,用这个币做测试,https://etherscan.io/address/0xD417144312DbF50465b1C641d016962017Ef6240,为了方便测试效果,我们把 permit 函数修改一下,添加一个 permit 只要验签成功则调用 _approve 授权,修改后的代码如下:

1

2

3

4

5

6

7

function permit2(address owner, address spender, uint256 amount, bytes32 _hash, uint8 v, bytes32 r, bytes32 s) public returns(address){

        address signer = ecrecover(_hash, v, r, s);

        require(signer != address(0) && signer == owner, "CovalentPermit: Invalid signature");

        _approve(owner, spender, amount);

    }

我们一直使用的是 remix 做测试,remix 上默认的账户如何导入到本地使用呢?需要找到它的私钥,通过了解发现 remix 的私钥是写死代码里的,可以查找 remix 的源码找到私钥,下面我们使用 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 这个地址来做测试。

1

2

3

4

5

6

7

Private key                                                         Address

503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb    0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

7e5bfb82febc4c2c8529167104271ceec190eafdca277314912eaabdb67c6e5f    0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

cc6d63f85de8fef05446ebdd3c537c72152d0fc437fd7aa62b3019b79bd1fdd4    0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db

638b5c6c8c5903b15f0d3bf5d3f175c64e6e98a10bdb9768a2003bf773dcb86a    0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB

......

在 geth 客户端使用 importRawKey 导入私钥,设置账户密码 123,然后再使用该账户签名 hello 这个字符串

1

2

3

4

5

6

>personal.importRawKey("503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb", "123");

>web3.personal.sign("0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8", "0x5b38da6a701c568545dcfcb03fcb875f56beddc4")

Give password for account 0x5b38da6a701c568545dcfcb03fcb875f56beddc4

Password:

"0x8f5786dc8f1ec71f2f9c1a1c49fb1fc7db248c92890ef4521fb07bf1bcc745644c66d9b7e5f420767931b830d1b41d9d36d0ee9daa7b6dec07e2d39a32ad0c5a1b"

需要将生成的签名数据进行拆分,还记得在上面的验签合约里的 splitSign 这个函数吗?它可以将签名拆分成 r, s, v 三个数据。

最后将相应的数据填写上,执行 permit2 无论是否使用 Owner 都可以执行这一笔授权操作,如下图所示:

为了确认授权是否成功,可以调用 ECR20 的 allowance 查看,返回的结果是 10,说明 permit2 是成功的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值