# 以太坊签名 # 以太坊公钥、私钥以及地址的生成 在以太坊中,每个用户都有一个私钥和公钥。其中公钥用于标识用户,因为公钥可生成以太坊地址。私钥用于对消息进行签名,因此私钥必须保密。 私钥就是一个256bit(32字节)的随机数,其范围为[1,n-1],其中n为secp256k1曲线的阶。注意,生成私钥必须脱离以太坊网络才能保证随机性。 在生成了私钥后,便可根据椭圆曲线算法(secp256k1)生成公钥,生成公式为: pubKey = privKey x G G是一个固定点,称为生成点 公钥的长度为64字节。 注意:由私钥生成公钥的过程是不可逆的。 在拥有了公钥后,可将其转换为以太坊地址,转化方式为:对PubKey进行keccak256后取后面20字节,并加上0x即可得到公钥对应的地址。 # 以太坊ECDSA签名 ECDSA签名一般由r,s组成,以太坊还添加了v字段,所以以太坊的签名包含r,s,v三个字段。 签名需要私钥和待签名的消息,签名的过程如下: 1. 计算待签名消息的hash,记作e 2. 安全的生成一个随机数k 3. 通过k x G得到一个点(x,y),该点也位于椭圆曲线中 4. 计算`r = x mod n`,r为0返回第二步。 5. 计算`s = k⁻¹(e + rdₐ) mod n`,s为0返回第二步。 注意:k是随机生成的,如果k不够随机,则可能通过两个不同的签名计算出私钥。 **r和s的长度都是32字节,而v的长度为1字节。一个签名的长度为65字节。** v是以太坊附加的字段,在EIP-155之前其值为0x1b或0x1c,之后需要根据`chainid`进行计算,公式为`chainId*2+35+{0,1}`。由于可根据r和s得到不同的椭圆曲线上的点,因此可得到两个不同的公钥,v用于指示该签名对应哪个公钥。 在已有签名后,可使用ecrecover函数,以rsv和e(待签名消息的hash)作为参数,最终恢复出对消息签名的地址。 # 以太坊交易签名 对交易签名无非将交易也当作消息进行签名,但是交易需要进行一些特殊处理,具体步骤如下: 1. 对`交易消息(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)`进行RLP编码。 2. 对上述RLP编码的结果进行Keccak256。 3. 对上述Keccak256的结果进行ECDSA私钥签名,得到`{r,s,v}`。 4. 将交易消息与`{r,s,v}`再次进行RLP编码,得到`RLP(nonce, gasPrice, gasLimit, to, value, data,v,r,s)`。 此处获得的`RLP(nonce, gasPrice, gasLimit, to, value, data,v,r,s)`即为已经签名的交易。 注意:此处已签名交易已经不包含chainId,因为v的计算是根据`chainId*2+35+{0,1}`计算得到的,因此可以根据v的值反推chainId。 对交易的验证也一样,需要交易消息和rsv使用ecrecover恢复签名者的地址,而后将签名者地址与交易中的from进行比较即可验证。 nonce和chainid:nonce代表账户所发送交易的数量,chainId标识了每条分叉链。二者结合,可以防止跨链重放攻击、双花攻击。 重放攻击与双花攻击的区别:重放攻击是盗用他人的签名获利,而双花攻击是重复使用自己的签名。 # 预签名 预签名即用户在链下将消息进行提取签名,而后将签名传递至链上项目中,链上项目根据签名恢复出消息以及签名者,从而降低开销。 # 以太坊签名规范-EIP191 以太坊签名规范规定了待签名数据的格式: ``` 0x19 <1 byte version> <version specific data> <data to sign>. ``` 1. 0x19保证了待签名肯定不是RLP编码,从而确保待签名消息不是一笔交易 2. version代表了签名的版本号,有0x00,0x01以及0x45 3. version specific data是特殊数据 4. data to sign,待签名的内容 以太坊共有三种签名: ## version 0x00 这种签名的格式为: ``` 0x19 <0x00> <intended validator address> <data to sign> ``` intended validator address为验证者的地址。 ## version 0x01 这种签名的格式为: ``` 0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign> ``` 注意:`<0x45 (E)> <thereum Signed Message:\n"`中,0x45是E的hex形式,因此实际上这里的内容可为: ``` 0x19 <Ethereum Signed Message:\n" + len(message)> <data to sign> ``` 使用`<Ethereum Signed Message:\n" + len(message)>`的目的是确保签名的消息只能用于以太坊。 ## version 0x45 EIP712 这种签名主要用于结构体消息的签名。 ### 签名过程 例如一个结构体 ```solidity struct Info{ address spender; uint256 number; } ``` 对于这个结构体,我们需要提供以下内容: 1. 域分隔符 2. 结构体的类型 3. 结构体的值 域分隔符用于确保签名的唯一有效性,不被重放。域分隔符的例子如下: ```js const domain = { name: "EIP712Storage", version: "1", chainId: "1", verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8", }; ``` 结构体的类型就是对结构体的定义,例子如下: ```js const types = { Storage: [ { name: "spender", type: "address" }, { name: "number", type: "uint256" }, ], }; ``` 结构体的值就是待签名的数据,例子如下: ```js const message = { spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", number: "100", }; ``` 有了以上三种内容,即可进行结构化签名,调用`signTypedData`,并传入域分隔符、结构体类型以及结构体值即可。 ### 验证过程 验证时都是在链上验证,因此验证过程主要包括: 1. 恢复被签名的信息 2. 使用ecrecover函数进行签名恢复得到签名者地址 构造被签名消息的代码如下: ```solidity bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(STRUCT_TYPEHASH,MemberValue...)) )); ``` 其中`STRUCT_TYPEHASH`是对结构体类型进行keccak256得到的值,例如: ```solidity bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)"); ``` MemberValue就是结构体内各元素的值。 在已有被签名消息的情况下,即可获得签名者。