以太坊公钥、私钥以及地址的生成在以太坊中,每个用户都有一个私钥和公钥。其中公钥用于标识用户,因为公钥可生成以太坊地址。私钥用于对消息进行签名,因此私钥必须保密。私钥就是一个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签名一般由r,s组成,以太坊还添加了v字段,所以以太坊的签名包含r,s,v三个字段。
签名需要私钥和待签名的消息,签名的过程如下:
r = x mod n
,r为0返回第二步。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)作为参数,最终恢复出对消息签名的地址。
对交易签名无非将交易也当作消息进行签名,但是交易需要进行一些特殊处理,具体步骤如下:
交易消息(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)
进行RLP编码。{r,s,v}
。{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标识了每条分叉链。二者结合,可以防止跨链重放攻击、双花攻击。
重放攻击与双花攻击的区别:重放攻击是盗用他人的签名获利,而双花攻击是重复使用自己的签名。
预签名即用户在链下将消息进行提取签名,而后将签名传递至链上项目中,链上项目根据签名恢复出消息以及签名者,从而降低开销。
以太坊签名规范规定了待签名数据的格式:
0x19 <1 byte version> <version specific data> <data to sign>.
以太坊共有三种签名:
这种签名的格式为:
0x19 <0x00> <intended validator address> <data to sign>
intended validator address为验证者的地址。
这种签名的格式为:
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)>
的目的是确保签名的消息只能用于以太坊。
这种签名主要用于结构体消息的签名。
例如一个结构体
struct Info{
address spender;
uint256 number;
}
对于这个结构体,我们需要提供以下内容:
域分隔符用于确保签名的唯一有效性,不被重放。域分隔符的例子如下:
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
,并传入域分隔符、结构体类型以及结构体值即可。
验证时都是在链上验证,因此验证过程主要包括:
构造被签名消息的代码如下:
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就是结构体内各元素的值。
在已有被签名消息的情况下,即可获得签名者。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!