Ethereum源码阅读笔记-accounts-3-创建账号背后的故事

转载请注明出处:www.huamo.online
字节杭州 求贤若渴:

  1. https://job.toutiao.com/s/JXTdQaH
  2. https://job.toutiao.com/s/JXTMWW3
  3. https://job.toutiao.com/s/JXT1tpC
  4. https://job.toutiao.com/s/JXTdu6h

new一个账号

1
2
3
$ geth account new --datadir "./datadir1"
password: 12345678
Address: {26add576232dad627a9102015a7b11763e98f85b}

这样就创建了一个新的账号,地址为26add576232dad627a9102015a7b11763e98f85b,密码为12345678keystore文件存在了./datadir1/keystore

address为20个字节,即16进制表示为40位。

背后的故事

入口:accountCreate()

cmd/geth/accountcmd.go中找到了geth account new命令的入口:

1
2
3
4
5
6
7
8
9
10
11
// cmd/geth/accountcmd.go

Name: "new",
Usage: "Create a new account",
Action: utils.MigrateFlags(accountCreate),
Flags: []cli.Flag{
utils.DataDirFlag,
utils.KeyStoreDirFlag,
utils.PasswordFileFlag,
utils.LightKDFFlag,
},

发现入口方法为accountCreate()。该方法

  1. 首先读取命令行输入的参数以更新geth中的各种配置

  2. 然后确定下来几个重要的参数:scryptN, scryptP, keydir,前两个用于加密生成的密钥,最后一个用于存放keystore文件。

  3. 获取密码:要么读取--password指定的密码文件第一行;要么让用户从命令行提供。

  4. 使用keystore.StoreKey()生成账户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// cmd/geth/accountcmd.go

// accountCreate根据命令行参数创建一个新账号放入keystore中。
func accountCreate(ctx *cli.Context) error {
// 生成默认配置
cfg := gethConfig{Node: defaultNodeConfig()}
// 如果命令行指定了--config并且对应文件能找到,则读取config文件更新cfg
if file := ctx.GlobalString(configFileFlag.Name); file != "" {
if err := loadConfig(file, &cfg); err != nil {
utils.Fatalf("%v", err)
}
}
// 使用命令行传入的参数更新节点相关的配置
// 比如上面我们指定了--datadir,那么就在这一步更新到节点配置中
utils.SetNodeConfig(ctx, &cfg.Node)

// 根据命令行的--lightkdf确定scrypt算法的参数,和keystore文件的存储目录
// scrypt:内存密集的密钥生成函数(KDF)
// 参见:https://tools.ietf.org/html/rfc7914
scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()

if err != nil {
utils.Fatalf("Failed to read configuration: %v", err)
}

// MakePasswordList可以读取--password指定的文件内容
// 文件内容按\n分隔,每一行内容再去掉尾部的\r
// 以此作为密码列表passwordList
// getPassPhrase传入的0,表示取passwordList[0]作为这个账号的密码(如果passwordList有内容的话)
// 传入true,表示如果是命令行输入密码,则需要重复确认一次,如果不一致,则创建账户直接失败。
password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))

// 最关键一步
// 生成密钥,并用刚才获取的password加密,然后存在keydir目录中
address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)

if err != nil {
utils.Fatalf("Failed to create account: %v", err)
}
fmt.Printf("Address: {%x}\n", address)
return nil
}

核心:storeNewKey()

进入keystore.StoreKey(),发现主要是调用了storeNewKey()方法生成密钥和地址,storeNewKey()方法传入了一个crand.Reader,一个跨平台的伪随机数产生器。

storeNewKey()在顶层做了全部的工作,包括:

  1. 调用newKey()生成私钥和账户地址。newKey()的实现包括:

    • 依赖于crand.Reader随机数产生器,使用ECDSA生成私钥privKey,该privKey结构体包含了pubKeyD,其中D非常重要,仅依靠D就能还原出对应的公私钥。

    • 依赖生成的pubKey计算出一个[20]byte,这就是以太坊的账户地址。

  2. 根据地址和前述的datadir参数,构建出以太坊的Account对象。

  3. 调用StoreKey()加密私钥,写入keystore文件中。StoreKey()的实现包括:

    • 基于scryptN, scryptP, password,使用scrypt算法生成加密KEY

    • 基于加密KEY,使用AES算法对生成的私钥key.D加密,只需要D就可以还原出公私钥。

    • 原子写入keystore文件,内容包括:

      • 明文地址

      • 明文UUID

      • keystore版本号3

      • 加密的D以及周边的解密线索。

  4. new一个账户整个过程结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// accounts/keystore/key.go

func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) {
key, err := newKey(rand)
if err != nil {
return nil, accounts.Account{}, err
}
// 基于创建的密钥,构建完整的Account对象
// keyFileName用来生成keystore文件名:UTC--当前时间--生成的账户地址
// Scheme:这里的账号后端协议模式都是"keystore"
// Scheme目前查到可取的值有ledger, trezor, keystore
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}}

// 加密密钥并写入keystore文件
if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
zeroKey(key.PrivateKey)
return nil, a, err
}
return key, a, err
}

func newKey(rand io.Reader) (*Key, error) {
// 基于随机数创建私钥,这个私钥是个结构体,包含了公钥
privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand)
if err != nil {
return nil, err
}
// 基于私钥生成Key结构体
// 结构体包含私钥,账户地址,uuid
return newKeyFromECDSA(privateKeyECDSA), nil
}

func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
id := uuid.NewRandom()
key := &Key{
Id: id,
// 公钥推导出地址
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
// 只存私钥,因为地址和公钥都能从私钥获取到。
// 而且这里总是存的私钥明文。
PrivateKey: privateKeyECDSA,
}
return key
}

通过公钥生成地址的方法由如下函数PubkeyToAddress()生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// crypto/crypto.go

func PubkeyToAddress(p ecdsa.PublicKey) common.Address {
pubBytes := FromECDSAPub(&p)
// BytesToAddress会将传入的[]byte b赋值给[20]byte a,作为账户地址
// 赋值规则如下:
// 1. 若len(b) > 20,则将最后20个字节复制更新a
// 2. 否则,将b的所有字节复制更新a的尾部部分。
return common.BytesToAddress(Keccak256(pubBytes[1:])[12:])
}

func FromECDSAPub(pub *ecdsa.PublicKey) []byte {
if pub == nil || pub.X == nil || pub.Y == nil {
return nil
}
return elliptic.Marshal(S256(), pub.X, pub.Y)
}

// Keccak256 calculates and returns the Keccak256 hash of the input data.
func Keccak256(data ...[]byte) []byte {
d := sha3.NewKeccak256()
for _, b := range data {
d.Write(b)
}
return d.Sum(nil)
}

最后一步就是加密密钥并写入keystore文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// accounts/keystore/keystore_passphrase.go

func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error {
// 基于password,scryptN, scryptP使用scrypt函数生成加密KEY
// 然后用这个加密KEY用AES算法对账户私钥key.D进行加密,写入keystore文件的crypto字段中。
// 因为以太坊用的ECDSA的公私钥都仅依赖于D参数,只要能拿到这个D,就能推算出公私钥
// 推算方法见crypto/crypto.go中的ToECDSAUnsafe()函数
// 日后使用password可以解密keystore文件获取私钥
// 因为scryptN, scryptP都明文写在了crypto.kdfparams.n, crypto.kdfparams.p中。
keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP)
if err != nil {
return err
}

// 以700权限创建keystore目录(若不存在)
// 以600权限先写入一个隐藏的临时文件:.{filename}.tmp
// 写成功后再改名,以保证原子写入
return writeKeyFile(filename, keyjson)
}

参考文章

转载请注明出处:www.huamo.online