# 以太坊签名
# 以太坊公钥、私钥以及地址的生成
在以太坊中,每个用户都有一个私钥和公钥。其中公钥用于标识用户,因为公钥可生成以太坊地址。私钥用于对消息进行签名,因此私钥必须保密。
私钥就是一个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就是结构体内各元素的值。
在已有被签名消息的情况下,即可获得签名者。