---
# System prepended metadata

title: 以太坊基础2-签名
tags: [以太坊基础]

---

# 以太坊签名
# 以太坊公钥、私钥以及地址的生成

在以太坊中，每个用户都有一个私钥和公钥。其中公钥用于标识用户，因为公钥可生成以太坊地址。私钥用于对消息进行签名，因此私钥必须保密。

私钥就是一个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就是结构体内各元素的值。

在已有被签名消息的情况下，即可获得签名者。

