以太坊签名详解

  • kaka111
  • 发布于 2025-03-01 20:53
  • 阅读 72

以太坊公钥、私钥以及地址的生成在以太坊中,每个用户都有一个私钥和公钥。其中公钥用于标识用户,因为公钥可生成以太坊地址。私钥用于对消息进行签名,因此私钥必须保密。私钥就是一个256bit(32字节)的随机数,其范围为[1,n-1],其中n为secp256k1曲线的阶。注意,生成私钥必须脱离以太坊网

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

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

私钥就是一个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

这种签名主要用于结构体消息的签名。

签名过程

例如一个结构体

struct Info{
    address spender;
    uint256 number;
}

对于这个结构体,我们需要提供以下内容:

  1. 域分隔符
  2. 结构体的类型
  3. 结构体的值

域分隔符用于确保签名的唯一有效性,不被重放。域分隔符的例子如下:

const domain = {
    name: "EIP712Storage",
    version: "1",
    chainId: "1",
    verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
};

结构体的类型就是对结构体的定义,例子如下:

const types = {
    Storage: [
        { name: "spender", type: "address" },
        { name: "number", type: "uint256" },
    ],
};

结构体的值就是待签名的数据,例子如下:

const message = {
    spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
    number: "100",
};

有了以上三种内容,即可进行结构化签名,调用signTypedData,并传入域分隔符、结构体类型以及结构体值即可。

验证过程

验证时都是在链上验证,因此验证过程主要包括:

  1. 恢复被签名的信息
  2. 使用ecrecover函数进行签名恢复得到签名者地址

构造被签名消息的代码如下:

   bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(STRUCT_TYPEHASH,MemberValue...))
        ));

其中STRUCT_TYPEHASH是对结构体类型进行keccak256得到的值,例如:

bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)");

MemberValue就是结构体内各元素的值。

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

点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
kaka111
kaka111
0xdea9...9f37
江湖只有他的大名,没有他的介绍。