sign a message 通常情况下,我们使用相同的私钥,对相同的消息签名,得到的签名是一样的,无论我们使用的是web3py, web3js还是ethersjs的工具库。
比如下面的python代码,就是生成了固定的签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from eth_account import Accountfrom web3 import Web3messagehash = Web3.keccak(text="Hello, world!" ) print ("message's hash" ,messagehash.hex ())privatekey ="0x0000000000000000000000000000000000000000000000000000000000000001" signMessage = Account.signHash(message_hash=messagehash, private_key=privatekey) print ("r = " , Web3.to_hex(signMessage.r))print ("s = " , Web3.to_hex(signMessage.s))print ("v = " , Web3.to_hex(signMessage.v))
ethersjs的工具库也是类似的:
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 const ethereumjsUtil = require ('ethereumjs-util' );const message = 'Hello, world!' ;const privateKey = Buffer .from ('0000000000000000000000000000000000000000000000000000000000000001' , 'hex' );const messageHash = ethereumjsUtil.keccak256 (message);const signature = ethereumjsUtil.ecsign (messageHash, privateKey);const formattedSignature = { v : signature.v , r : signature.r .toString ('hex' ), s : signature.s .toString ('hex' ) }; console .log ('Message:' , message);console .log ('Message Hash:' , messageHash.toString ('hex' ));console .log ('Signature:' , formattedSignature);
这样看来,所有封装好的工具库都是得到相同的结果,这也印证了平时我们的认知:相同的私钥对相同的消息,只能生成相同的签名。其实不然,这些封装好的工具库其实是对其中一个参数固定了不取值:临时密钥。如果这个值修改,得到的签名就会不同。
比如在上面代码的js版本中,是通过ethereumjs-util这个库进行签名的,我们来追溯源码,最终找到:node_modules\ethereumjs-util\dist\signature.js:
1 2 3 4 5 6 7 8 9 10 exports .ecsign = function (msgHash, privateKey, chainId ) { var sig = secp256k1.sign (msgHash, privateKey}); var recovery = sig.recovery ; var ret = { r : sig.signature .slice (0 , 32 ), s : sig.signature .slice (32 , 64 ), v : chainId ? recovery + (chainId * 2 + 35 ) : recovery + 27 , }; return ret; };
还有node_modules\ethereumjs-util\dist\secp256k1v3-adapter.js:
1 2 3 exports .sign = function (message, privateKey, options ) { ...... };
可以看到,secp256k1算法是还有一个参数options的,在ecsign实现的时候,这个值并没有传,使用了默认值(各大封装好的工具库都是一样使用这个默认值,好像是啥都没填写,没找到具体值,似乎是undifined)。那么如果我们将options给一个随机值,得到的签名就会不同,因此我们修改源码:
1 2 3 4 5 6 7 8 9 10 11 const { randomBytes } = require ('crypto' );exports .ecsign = function (msgHash, privateKey, chainId ) { var sig = secp256k1.sign (msgHash, privateKey,{data : randomBytes (32 )}); var recovery = sig.recovery ; var ret = { r : sig.signature .slice (0 , 32 ), s : sig.signature .slice (32 , 64 ), v : chainId ? recovery + (chainId * 2 + 35 ) : recovery + 27 , }; return ret; };
这样,我们每次签名,得到的结果都不一样了,成功做到:相同私钥,相同消息,不同签名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [Running] node "d:\ethersjs\practice\capturetheether.js" Message: Hello, world! Message Hash: b6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4 Signature: { v: 27, r: '4706db97b1d93ceb09919e76cd964574c6742b78dd0052899b2e0988710abbd8' , s: '48a4bcf9d488aa5531f27adeef52388021461139eea8db19cc5bf73d8162ce21' } [Done] exited with code=0 in 0.386 seconds [Running] node "d:\ethersjs\practice\capturetheether.js" Message: Hello, world! Message Hash: b6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4 Signature: { v: 28, r: 'c192b6a1e32ff0c3400632f5bf5e196a2c9d7abf05884ea9b4de3cfabfd2d04a' , s: '446c55c8a713162564aeb3216fe1003ad901ff6ed7aef89d6f841c722bf73326' }
我们知道,在solidity预编译的ecRecover合约的ecdsa算法中,使用的是secp256k1算法,临时密钥options,会影响到它验证吗?回答:不会影响。因为顾名思义,临时密钥options,只是一个临时性的东西,他取任何值都可以,不会影响结果的有效性。这就意味着,用相同的私钥,签名相同的消息,得到不同的签名结果,这些签名都是有效的,可以通过ecrecover的验证。