Renaissance Labs

Posted on Apr 13, 2022Read on Mirror.xyz

Mumbler

基础---很多人并没有搞明白

以太坊私钥

eg:
fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19

由256位;不考虑0x前缀,友32个字节,64个字符构成。

以太坊公共地址

eg:

0x20F8D42FB0F667F2E53930fed426f225752453b3

由160位组成;不考虑0x前缀,由20个字节,40个字符构成。

公钥与以太坊公共地址是一回事吗

一句话,公共地址是公钥的Keccak-256哈希。

具体在以太坊中,用keccak哈希算法来计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。

所以,在以太坊中它俩个不是一回事。

以太坊地址的大小写---到底有没有区别

一个现象是我们用钱包的时候,比如ImToken/Metamask/交易所,钱包地址都是大小写混杂的,但是在etherscan和opensea上查看交易信息和NFT信息时却可以不区分大小写的正确进行查询,这是为什么呢?

还得从以太坊历史说起,起初以太坊内部实际并不区分大小写的,但是用户在钱包的实际使用过程中,以太坊的用户发现,由于缺乏类似比特币的自带地址校验机制,以太坊的公开地址在当朋友之间转账时,经常因为误输入字符而转错了账号。

于是社区用户在以太坊很早期的时候,就已经提出该问题并思考改进的方法,经过整理成文,形成了 EIP-55 提议。该提议按照一定逻辑,将地址中的部分字母大写,与剩余的小写字母来形成校验和,让地址拥有自校验的能力,具体JavaScript实现代码如下:

const createKeccakHash = require('keccak')

function toChecksumAddress (address) {
  address = address.toLowerCase().replace('0x', '')
  var hash = createKeccakHash('keccak256').update(address).digest('hex')
  var ret = '0x'

  for (var i = 0; i < address.length; i++) {
    if (parseInt(hash[i], 16) >= 8) {
      ret += address[i].toUpperCase()
    } else {
      ret += address[i]
    }
  }

  return ret
}

即符合EIP55规范的软件或APP应该对传入的地址进行一次检验,以防止人为书写错误。进一步实际上对账户地址进行keccak256哈希运算,根据运算结果值的二进制位图决定地址十六进制字符串(0-9,a-f)的大写或小些。

带校验地址 (大小写区分)0x7c52e508C07558C287d5A453475954f6a547eC41 普通地址 (全小写)0x7c52e508c07558c287d5a453475954f6a547ec41

优点:

  • 向后兼容许多接受混合大小写的十六进制解析器,允许它随着时间的推移轻松引入
  • 保持长度为40个字符
  • 平均每个地址将有15个校验位,如果输入错误,随机生成的地因抄写错误意外通过检查的净概率为0.0247%。 这比ICAP提高了约50倍,但不如4字节检查代码好。

BIP39助记词与私钥---有怎样的转换为关系

首先要说明的是BIP39助记词有12、15、18、24个区分,但实际原理是一样的,一般钱包更常见的是采用12个助记词。下面以12个助记词来讲解原理:

在实际应用中,助记词往往不是匹配到一个私钥的,常常被使用的是层级钱包,也就是HD钱包,hierarchical deterministic wallet。这种模式中,助记词对应的是原始密码,原始密码可以生成种子秘钥,种子秘钥可以能再生成多级的子公私钥对。这种模式的好处在于,有了助记词,就能获得所有子账户的控制权。同时,子账户中的私钥可以从未生成的和传播,有利于钱包安全。

那么助记词的原理是什么呢?

我们都知道,以太坊的私钥,看起来是256位的二进制字符串。不便于记忆,也不便于输入。为了便于记忆,我们可以把特定的字符组合映射到某个单词,这样就便于记忆了。

例如,3个字符的组合有2的3次方,一共8种。我们可以设定一种关系,000用moon表示,001用sun表示,010用zero表示,011用zoom表示。这样一个12个字符的串,000010011001我们就可以及意义成 moon sun zero zoom 了,这样输入时不容易出错,也容易记忆。具体原理图如下:

以太坊私钥到公钥---具体实现原理

首先从一段代码看起,这样整个过程会更清晰

package main

import (
	"crypto/ecdsa"
	"fmt"
	"log"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/crypto"
	"golang.org/x/crypto/sha3"
)

func main() {
	privateKey, err := crypto.GenerateKey()
	if err != nil {
		log.Fatal(err)
	}
	privateKeyBytes := crypto.FromECDSA(privateKey)
	fmt.Println(hexutil.Encode(privateKeyBytes)[2:]) 

	publicKey := privateKey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
	}
	
	publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
	fmt.Println(hexutil.Encode(publicKeyBytes)[4:]) 

	address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
	fmt.Println(address)

	//以下是使用 golang.org/x/crypto/sha3 的 Keccak256函数手动完成的方法。会在keccak环节深入讲解。
	hash := sha3.NewLegacyKeccak256()
	hash.Write(publicKeyBytes[1:])
	fmt.Println(hexutil.Encode(hash.Sum(nil)[12:])) // 0x96216849c49358b10257cb55b28ea603c874b05e
}

解析:

  • crypto.GenerateKey() //生成随机私钥

  • crypto.FromECDSA(privateKey) //将其转换为字节, 再次强调一个字节等于2个字符,所以普通的以太坊地址除了0x外是40个字符,20个字节。这个过程完成的ECDSA签名,具体实现的功能在下一节讲解。

  • fmt.Println(hexutil.Encode(privateKeyBytes)[2:]) // 转换为十六进制字符串,该包提供了一个带有字节切片的Encode方法,然后我们在十六进制编码之后删除“0x”; 私钥是256位的二进制字符串,也就是32个字节,64个字符。

  • publicKey := privateKey.Public() //由于公钥是从私钥派生的,加密私钥具有一个返回公钥的Public方法

  • publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) //这一步就是EIP55做的事情,生成经过ECDSA校验的Public

  • publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) //将其转换为字节

  • fmt.Println(hexutil.Encode(publicKeyBytes)[4:]) // 9a7df67f79246283fdc93af76d4f8cdd62c4886e8cd870944e817dd0b97934fdd7719d0810951e03418205868a5c1b40b192451367f28e0088dd75e15de40c05 将其转换为十六进制的过程与我们使用转化私钥的过程类似。 我们剥离了0x和前2个字符04,它始终是EC前缀,不是必需的。

  • address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() //接受一个ECDSA公钥,并返回公共地址; 公共地址其实就是公钥的Keccak-256哈希,然后我们取最后40个字符(20个字节)并用“0x”作为前缀

  • fmt.Println(address) // 0x96216849c49358B10257cb55b28eA603c874b05E

    最终,打印出来的0x96216849c49358B10257cb55b28eA603c874b05E的地址就是我们钱包所见的公钥地址了。

不得不说的ECDSA

上面的章节中我们看到了通过代码crypto.FromECDSA(privateKey) 来执行ECDSA签名,那么ECDSA是什么?到底完成了什么功能?

ECDSA定义

ECDSA又名椭圆曲线数字签名算法,是使用椭圆曲线密码(ECC)对数字签名算法(DSA)的模拟。数字签名算法(DSA)是联邦信息处理标准FIPS中的数字签名标准,它的安全性基于素域上的离散对数问题。椭圆曲线密码(ECC)由Neal Koblitz和Victor Miller于1985年发明。它可以看作是椭圆曲线对先前基于离散对数问题(DLP)的密码系统的模拟,只是群元素由素域中的元素数换为有限域上的椭圆曲线上的点。

一句话总结,ECDSA是ECC与DSA的结合,整个签名过程与DSA类似,所不一样的是签名中采取的算法为ECC,最后签名出来的值也是分为r,s。

ECDSA功能

  • 初始化化秘钥组,生成ECDSA算法的公钥和私钥

  • 执行私钥签名, 使用私钥签名,生成私钥签名

  • 执行公钥签名,生成公钥签名

  • 使用公钥验证私钥签名 

    Tips:ECDSA属于非对称加密。公钥与私钥匙成对出现。 遵从的原则就是“私钥签名、公钥验证”。

ECDSA原理(签名+验签)

  • 签名原理: 1、选择一条椭圆曲线Ep(a,b),和基点G; 2、选择私有密钥k(k<n,n为G的阶),利用基点G计算公开密钥K=kG; 3、产生一个随机整数r(r<n),计算点R=rG; 4、将原数据和点R的坐标值x,y作为参数,计算SHA1做为hash,即Hash=SHA1(原数据,x,y); 5、计算s≡r - Hash * k (mod n) 6、r和s做为签名值,如果r和s其中一个为0,重新从第3步开始执行
  • 验证原理: 1、接受方在收到消息(m)和签名值(r,s)后,进行以下运算 2、计算:sG+H(m)P=(x1,y1), r1≡ x1 mod p。 3、验证等式:r1 ≡ r mod p。 4、如果等式成立,接受签名,否则签名无效。

参考

1.https://ethbook.abyteahead.com/ch2/eip55.html

2.https://learnblockchain.cn/docs/eips/eip-55.html#%E8%A7%84%E8%8C%83 EIP55

3.https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md EIP55

4.https://zhuanlan.zhihu.com/p/150125861 助记词到私钥

5.https://blog.csdn.net/m0_37458552/article/details/80250258 ECDSA