geth子命令源码分析:init、account

geth总览

首先看一下geth这个程序的总体设计,其主要代码位于go-ethereum\cmd\geth里面,先看main.go,这是一个利用urfave/cli开发的命令行程序,关于这个库的简单介绍见这里。在开头var代码块中中实例化了cli的app对象:

app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
func NewApp(gitCommit, usage string) *cli.App {
	app := cli.NewApp()
	app.Name = filepath.Base(os.Args[0])
	app.Author = ""
	//app.Authors = nil
	app.Email = ""
	app.Version = params.VersionWithMeta //见go-ethereum\params\version.go 生成版本号
	if len(gitCommit) >= 8 {
		app.Version += "-" + gitCommit[:8] //gitCommit之前在编译时定义
	}
	app.Usage = usage
	return app
}

实例化之后,定义了大量flag,详细信息可以输入geth -h或到官方文档查看。接下来,在init()方法中进行进一步初始化,添加了大量command及先前定义的flag。并定义了geth的action:

app.Action = geth
func geth(ctx *cli.Context) error {
	if args := ctx.Args(); len(args) > 0 { 
		return fmt.Errorf("invalid command: %q", args[0])
	}
	node := makeFullNode(ctx) 
	defer node.Close()
	startNode(ctx, node) 
	node.Wait()
	return nil
}

这是关于geth启动流程的代码,我们后续再分析。在init中还定义了app.Before用于初始化工作:

app.Before = func(ctx *cli.Context) error {
		logdir := ""
		if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
			logdir = (&node.Config{DataDir: utils.MakeDataDir(ctx)}).ResolvePath("logs")
		}
		if err := debug.Setup(ctx, logdir); err != nil { //见\go-ethereum\internal\debug\flags.go
			return err
		}
		// Cap the cache allowance and tune the garbage collector
		var mem gosigar.Mem //见\go-ethereum\vendor\github.com\elastic\gosigar\sigar_interface.go
		//获取系统内存信息
		if err := mem.Get(); err == nil { //配置缓存
			allowance := int(mem.Total / 1024 / 1024 / 3)
			if cache := ctx.GlobalInt(utils.CacheFlag.Name); cache > allowance {
				log.Warn("Sanitizing cache to Go's GC limits", "provided", cache, "updated", allowance)
				ctx.GlobalSet(utils.CacheFlag.Name, strconv.Itoa(allowance))
			}
		}
		// Ensure Go's GC ignores the database cache for trigger percentage
		cache := ctx.GlobalInt(utils.CacheFlag.Name)
		gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))

		log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc))
		godebug.SetGCPercent(int(gogc)) //设置垃圾收集目标百分比

		// Start metrics export if enabled
		utils.SetupMetrics(ctx) //默认是关闭的,开关在\go-ethereum\metrics\metrics.go

		// Start system runtime metrics collection
		go metrics.CollectProcessMetrics(3 * time.Second) //默认不会启动

		return nil
	}

随后也定义了app.After逻辑

app.After = func(ctx *cli.Context) error {
		debug.Exit()
		console.Stdin.Close() // 重置终端模式
		return nil
	}

接下来进入main函数

func main() {
	if err := app.Run(os.Args); err != nil { //运行处理程序
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

到这里,geth大致轮廓就看完了,随后会根据用户输入的命令执行相应的逻辑。

init

这是初始化函数。源码中描述如下

The init command initializes a new genesis block and definition for the network.
This is a destructive action and changes the network in which you will be
participating.
It expects the genesis file as argument.

一般使用如下

geth init gen.json --datadir ./mychain/

需要指定一个json文件,可选择指定数据存储路径。在源码定义如下:

// go-ethereum\cmd\geth\chaincmd.go
initCommand = cli.Command{
		Action:    utils.MigrateFlags(initGenesis),
		Name:      "init",
		Usage:     "Bootstrap and initialize a new genesis block",
		ArgsUsage: "<genesisPath>",
		Flags: []cli.Flag{
			utils.DataDirFlag,
		},
		Category: "BLOCKCHAIN COMMANDS",
		Description: `.....`,
	}

可见子命令名就是init,没有别名,只有一个flag,定义如下

// go-ethereum\cmd\utils\flags.go
DataDirFlag = DirectoryFlag{
		Name:  "datadir",
		Usage: "Data directory for the databases and keystore",
		Value: DirectoryString{node.DefaultDataDir()}, //获取默认目录,见go-ethereum\node\defaults.go
		//linux下为/home/<user>/.ethereum
	}

DirectoryString定义如下

// go-ethereum\cmd\utils\customflags.go
type DirectoryString struct {
	Value string
}

func (self *DirectoryString) String() string {
	return self.Value
}

func (self *DirectoryString) Set(value string) error {
	self.Value = expandPath(value)
	return nil
}
func expandPath(p string) string {
	if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
		if home := homeDir(); home != "" {
			p = home + p[1:]
		}
	}
	return path.Clean(os.ExpandEnv(p))
}

DataDirFlag作用主要就是定义初始化生成数据的存储路径,之后我们看initCommand的action,这个是关键:

Action:    utils.MigrateFlags(initGenesis),

调用了utils.MigrateFlags,定义如下

// \go-ethereum\cmd\utils\flags.go
func MigrateFlags(action func(ctx *cli.Context) error) func(*cli.Context) error {
	return func(ctx *cli.Context) error {
		for _, name := range ctx.FlagNames() {
			if ctx.IsSet(name) {
				ctx.GlobalSet(name, ctx.String(name))
			}
		}
		return action(ctx)
	}
}

这个方法并没有什么实际意义,只是将所有用户指定的flag以GlobalSet的形式存了起来,主要逻辑在传递进来的action,我们这里传递的action如下:

// go-ethereum\cmd\geth\chaincmd.go
func initGenesis(ctx *cli.Context) error {
	genesisPath := ctx.Args().First() 
	if len(genesisPath) == 0 {
		utils.Fatalf("Must supply path to genesis JSON file")
	}
	file, err := os.Open(genesisPath)
	if err != nil {
		utils.Fatalf("Failed to read genesis file: %v", err)
	}
	defer file.Close()

	genesis := new(core.Genesis) 
	if err := json.NewDecoder(file).Decode(genesis); err != nil { 
		utils.Fatalf("invalid genesis file: %v", err)
	}

	stack := makeFullNode(ctx) 
	defer stack.Close()

	for _, name := range []string{"chaindata", "lightchaindata"} {
		chaindb, err := stack.OpenDatabase(name, 0, 0) 
		if err != nil {
			utils.Fatalf("Failed to open database: %v", err)
		}
		_, hash, err := core.SetupGenesisBlock(chaindb, genesis) 
		if err != nil {
			utils.Fatalf("Failed to write genesis block: %v", err)
		}
		log.Info("Successfully wrote genesis state", "database", name, "hash", hash)
	}
	return nil
}

第一行获取我们传入的json文件路径,然后代开这个文件,并解析成一个创世区块对象,中间有一步出错就返回。到这里已经根据我们的json文件创建了一个创世区块对象genesis。接着使用makeFullNode创建了一个节点,这个在分析geth启动流程时详细分析,这里只用知道他给我们返回一个node对象,代表一个节点。

接着遍历了一个字符串数组,为的是创建数据库目录,用的是OpenDatabase方法,最终创建的两个数据库默认情况分别在datadir/geth/chaindata,datadir/geth/lightchaindata。之后调用SetupGenesisBlock方法写入创世区块,这个方法在我们F分析创世区块时分析过,就是把我们定义的创世区块内如写入数据库,最后打印log。

account

这是账户相关的自命令,定义如下

accountCommand = cli.Command{
		Name:     "account",
		Usage:    "Manage accounts",
		Category: "ACCOUNT COMMANDS",
		Description: ``,
		Subcommands: []cli.Command{
			{
				Name:   "list",
				Usage:  "Print summary of existing accounts",
				Action: utils.MigrateFlags(accountList),
				Flags: []cli.Flag{
					utils.DataDirFlag,
					utils.KeyStoreDirFlag,
				},
				Description: `...`,
			},
			{
				Name:   "new",
				Usage:  "Create a new account",
				Action: utils.MigrateFlags(accountCreate),
				Flags: []cli.Flag{
					utils.DataDirFlag,
					utils.KeyStoreDirFlag,
					utils.PasswordFileFlag,
					utils.LightKDFFlag,
				},
				Description: `...`,
			},
			{
				Name:      "update",
				Usage:     "Update an existing account",
				Action:    utils.MigrateFlags(accountUpdate),
				ArgsUsage: "<address>",
				Flags: []cli.Flag{
					utils.DataDirFlag,
					utils.KeyStoreDirFlag,
					utils.LightKDFFlag,
				},
				Description: `...`,
			},
			{
				Name:   "import",
				Usage:  "Import a private key into a new account",
				Action: utils.MigrateFlags(accountImport),
				Flags: []cli.Flag{
					utils.DataDirFlag,
					utils.KeyStoreDirFlag,
					utils.PasswordFileFlag,
					utils.LightKDFFlag,
				},
				ArgsUsage: "<keyFile>",
				Description: `...`,
			},
		},
	}

这个命令的名称就是account,没有别名也没有直接的flag,但是有几个二级命令

list

这个命令的作用是列出所有的账户,有两个flag。DataDirFlag用来指定节点目录,使用–datadir指定;KeyStoreDirFlag用来指定秘钥存储路径,使用–keystoreh指定。改名了的动作如下,注意还是MigrateFlags包裹一个方法的形式,直接看动作

func accountList(ctx *cli.Context) error {
	stack, _ := makeConfigNode(ctx)
	var index int
	for _, wallet := range stack.AccountManager().Wallets() {
		for _, account := range wallet.Accounts() {
			fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
			index++
		}
	}
	return nil
}

很简单就是首先使用makeConfigNode构造一个节点,这里之所以不用makeFullNode是因为不必注册服务。之后获取节点的账户管理者,再获取所有钱包,遍历每个钱包的账户打印出来即可。

new

这是新建一个账户的命令,命令名就是new,有四个flag。前两个和list的一样。后面的,PasswordFileFlag是用来指定账户密码的文件,使用–password指定,默认为空。LightKDFFlag是表示是否要降低秘钥生成时cpu和ram的用量,是一个布尔类型,需要的话使用–lightkdf开启。我们接着看主要逻辑

func accountCreate(ctx *cli.Context) error {
	cfg := gethConfig{Node: defaultNodeConfig()}

	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
		if err := loadConfig(file, &cfg); err != nil {
			utils.Fatalf("%v", err)
		}
	}
	utils.SetNodeConfig(ctx, &cfg.Node)
	scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()

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

	password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))

	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
}

开始时施加在默认及用户配置,然后设置配置,接着调用AccountConfig获取了几个变量,分别是scrypt算法的N参数scryptN,scrypt算法的P参数scryptP,秘钥存储目录及错误。没有错误的话,调用getPassPhrase获取密码

func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string {
	if len(passwords) > 0 {
		if i < len(passwords) {
			return passwords[i]
		}
		return passwords[len(passwords)-1]
	}

	if prompt != "" {
		fmt.Println(prompt)
	}
	password, err := console.Stdin.PromptPassword("Passphrase: ")
	if err != nil {
		utils.Fatalf("Failed to read passphrase: %v", err)
	}
	if confirmation {
		confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
		if err != nil {
			utils.Fatalf("Failed to read passphrase confirmation: %v", err)
		}
		if password != confirm {
			utils.Fatalf("Passphrases do not match")
		}
	}
	return password
}

func MakePasswordList(ctx *cli.Context) []string {
	path := ctx.GlobalString(PasswordFileFlag.Name)
	if path == "" {
		return nil
	}
	text, err := ioutil.ReadFile(path)
	if err != nil {
		Fatalf("Failed to read password file: %v", err)
	}
	lines := strings.Split(string(text), "\n")

	for i := range lines {
		lines[i] = strings.TrimRight(lines[i], "\r")
	}
	return lines
}

MakePasswordList方法是从我们用–password指定的文件中读取密码,该文件每一行表示一个密码。在getPassPhrase中,刚才读取的密码个数大于0,则取第一个作为密码。否则进入一个交互环境,让用户输入密码,用的是PromptPassword方法,之后还要输入第二遍进行验证。回到accountCreate中,取到密码后使用StoreKey生产账户

func StoreKey(dir, auth string, scryptN, scryptP int) (common.Address, error) {
	_, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP, false}, rand.Reader, auth)
	return a.Address, err
}

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
	}
	a := accounts.Account{
		Address: key.Address,
		URL:     accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))},
	}
	if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
		zeroKey(key.PrivateKey)
		return nil, a, err
	}
	return key, a, err
}

直接调用的是storeNewKey方法,不要过创建了一个keyStorePassphrase对象,他实现了keyStore接口。storeNewKey方法先调用了newKey方法生成椭圆曲线加密的私钥,并转为Key对象

func newKey(rand io.Reader) (*Key, error) {
	privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand)
	if err != nil {
		return nil, err
	}
	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
}

注意这里实际上也就是生成了账户地址,就是私钥对应的公钥,不过要简单处理一下

func PubkeyToAddress(p ecdsa.PublicKey) common.Address {
	pubBytes := FromECDSAPub(&p)
	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哈希运算,然后取后20个字节作为账户地址。

newKeyFromECDSA最后返回的是一个Key对象,还包含一个16字节的随机编号。回到storeNewKey中,此时组建一个account对象,表示一个账户,包含该账户的地址以及url,格式是keystore://文件名。文件名如下生成

func keyFileName(keyAddr common.Address) string {
	ts := time.Now().UTC()
	return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:]))
}

有两部分组成,第一部分是创建时间,第二部分是地址的16进制表示。接着调用StoreKey方法存储秘钥,这是keyStorePassphrase的方法

func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error {
	keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP)
	if err != nil {
		return err
	}
	// Write into temporary file
	tmpName, err := writeTemporaryKeyFile(filename, keyjson)
	if err != nil {
		return err
	}
	if !ks.skipKeyFileVerification {
		// Verify that we can decrypt the file with the given password.
		_, err = ks.GetKey(key.Address, tmpName, auth)
		if err != nil {
			msg := "An error was encountered when saving and verifying the keystore file. \n" +
				"This indicates that the keystore is corrupted. \n" +
				"The corrupted file is stored at \n%v\n" +
				"Please file a ticket at:\n\n" +
				"https://github.com/ethereum/go-ethereum/issues." +
				"The error was : %s"
			return fmt.Errorf(msg, tmpName, err)
		}
	}
	return os.Rename(tmpName, filename)
}

第一步加密秘钥,

func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
	keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32)
	cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP)
	if err != nil {
		return nil, err
	}
	encryptedKeyJSONV3 := encryptedKeyJSONV3{
		hex.EncodeToString(key.Address[:]),
		cryptoStruct,
		key.Id.String(),
		version,
	}
	return json.Marshal(encryptedKeyJSONV3)
}

PaddedBigBytes方法是将椭圆加密的私钥中D参数以大端形式表示成一个字节数组。然后加密数据,使用我们给账户指定的密码加密椭圆曲线的私钥中的D参数

func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) {

	salt := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		panic("reading from crypto/rand failed: " + err.Error())
	}
	derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen)
	if err != nil {
		return CryptoJSON{}, err
	}
	encryptKey := derivedKey[:16]

	iv := make([]byte, aes.BlockSize) // 16
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		panic("reading from crypto/rand failed: " + err.Error())
	}
	cipherText, err := aesCTRXOR(encryptKey, data, iv)
	if err != nil {
		return CryptoJSON{}, err
	}
	mac := crypto.Keccak256(derivedKey[16:32], cipherText)

	scryptParamsJSON := make(map[string]interface{}, 5)
	scryptParamsJSON["n"] = scryptN
	scryptParamsJSON["r"] = scryptR
	scryptParamsJSON["p"] = scryptP
	scryptParamsJSON["dklen"] = scryptDKLen
	scryptParamsJSON["salt"] = hex.EncodeToString(salt)
	cipherParamsJSON := cipherparamsJSON{
		IV: hex.EncodeToString(iv),
	}

	cryptoStruct := CryptoJSON{
		Cipher:       "aes-128-ctr",
		CipherText:   hex.EncodeToString(cipherText),
		CipherParams: cipherParamsJSON,
		KDF:          keyHeaderKDF,
		KDFParams:    scryptParamsJSON,
		MAC:          hex.EncodeToString(mac),
	}
	return cryptoStruct, nil
}

这一段逻辑是这样的,首先生成一个salt,然后利用scrypt算法生成一个导出秘钥,scrypt算法是可以抗暴力破解的,我们利用得到的导出秘钥的前16位作为真正的加密秘钥encryptKey,然后使用CTR模式的AES加密算法,用encryptKey加密私钥的餐数D,最后得到密文。为了能正确解密,我们需要记录一些内容,如scrypt算法的参数N、R、P,导出秘钥的长度dklen,随机变量salt,CTR模式的初始化向量IV(实际是空数组),加密算法,密文,秘钥导出算法名称,最后还有一个mac用来验证数据完整及正确性,它是用导出秘钥的第16到第32字节以及密文经过Keccak256运算后的结果。

最后返回一个cryptoStruct对象,他没有包含我们设定的密码。获得最后私钥的关键是获得导出秘钥,若没有密码,只能暴力破解,但是scrypt算法是可以抵抗暴力破解的,所以是比较安全的保存私钥方法。

回到EncryptKey中,我们最后构建一个encryptedKeyJSONV3对象,他除了包含我们刚才与加密有关的cryptoStruct对象,还包含账户地址,ID以及版本号。最后将encryptedKeyJSONV3序列化位Json数据。

回到StoreKey,经过EncryptKey方法我们已经得到了json数据,此时现将其写入一个临时文件中,然后根据需求进行解密测试

func (ks keyStorePassphrase) GetKey(addr common.Address, filename, auth string) (*Key, error) {
	keyjson, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	key, err := DecryptKey(keyjson, auth)
	if err != nil {
		return nil, err
	}
	if key.Address != addr {
		return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr)
	}
	return key, nil
}

func DecryptKey(keyjson []byte, auth string) (*Key, error) {
	m := make(map[string]interface{})
	if err := json.Unmarshal(keyjson, &m); err != nil {
		return nil, err
	}

	var (
		keyBytes, keyId []byte
		err             error
	)
	if version, ok := m["version"].(string); ok && version == "1" {
		k := new(encryptedKeyJSONV1)
		if err := json.Unmarshal(keyjson, k); err != nil {
			return nil, err
		}
		keyBytes, keyId, err = decryptKeyV1(k, auth)
	} else {
		k := new(encryptedKeyJSONV3)
		if err := json.Unmarshal(keyjson, k); err != nil {
			return nil, err
		}
		keyBytes, keyId, err = decryptKeyV3(k, auth)
	}

	if err != nil {
		return nil, err
	}
	key := crypto.ToECDSAUnsafe(keyBytes)

	return &Key{
		Id:         uuid.UUID(keyId),
		Address:    crypto.PubkeyToAddress(key.PublicKey),
		PrivateKey: key,
	}, nil
}

func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) {
	if keyProtected.Version != version {
		return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version)
	}
	keyId = uuid.Parse(keyProtected.Id)
	plainText, err := DecryptDataV3(keyProtected.Crypto, auth)
	if err != nil {
		return nil, nil, err
	}
	return plainText, keyId, err
}

func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) {
	if cryptoJson.Cipher != "aes-128-ctr" {
		return nil, fmt.Errorf("Cipher not supported: %v", cryptoJson.Cipher)
	}
	mac, err := hex.DecodeString(cryptoJson.MAC)
	if err != nil {
		return nil, err
	}

	iv, err := hex.DecodeString(cryptoJson.CipherParams.IV)
	if err != nil {
		return nil, err
	}

	cipherText, err := hex.DecodeString(cryptoJson.CipherText)
	if err != nil {
		return nil, err
	}

	derivedKey, err := getKDFKey(cryptoJson, auth)
	if err != nil {
		return nil, err
	}

	calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText)
	if !bytes.Equal(calculatedMAC, mac) {
		return nil, ErrDecrypt
	}

	plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv)
	if err != nil {
		return nil, err
	}
	return plainText, err
}

实际上就是加密的逆过程,先读取json数据,然后先根据版本号执行不同的解密算法,我们只看V3版本的。在decryptKeyV3中,先验证版本号,然后解析出ID,之后利用DecryptDataV3解密,解密过程是先判断加密方法是否正确,然后解析出mac、iv和密文。先利用getKDFKey方法获取导出秘钥,只有提供的密码正确才能得到正确结果,然后验证mac,在进行解密获得明文。根据明文获得秘钥,再根据秘钥获得公钥,最后组装一个Key对象。然后回到GetKey中,比较提供的地址和我们解密计算出的地址是否一样,一样的话返回key,没有错误表示解密验证成功。

最后回到StoreKey将临时文件重命名为正式文件。这样新建一个账户的工作完成。回顾一下,实际上主要是随机生成了一对椭圆曲线秘钥,然后将私钥用我们提供的密码加密并保存成文件,生成秘钥的时候生成了地址。

回到accountCreate中,StoreKey方法返回了账户地址,最后打印出来即可。

update

这是用来更新一个账户,所谓的更新是用新版本的存储为新版本的加密文件。有三个flag,分别是DataDirFlag,KeyStoreDirFlag以及LightKDFFlag,前面都介绍过了,注意该命令还要指定地址。使用格式如下:geth account update [options] <address>,我们看他的action

func accountUpdate(ctx *cli.Context) error {
	if len(ctx.Args()) == 0 {
		utils.Fatalf("No accounts specified to update")
	}
	stack, _ := makeConfigNode(ctx)
	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)

	for _, addr := range ctx.Args() {
		account, oldPassword := unlockAccount(ctx, ks, addr, 0, nil)
		newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
		if err := ks.Update(account, oldPassword, newPassword); err != nil {
			utils.Fatalf("Could not update the account: %v", err)
		}
	}
	return nil
}

首先判断我们是否制定了地址参数,没有的话报错,接着获取了一个节点,然后从节点的账户管理者那里获取了一个keystore类型的backend。有可能我们指定了多个地址,所以遍历所有地址,首先对每个地址执行解锁操作

func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
	account, err := utils.MakeAddress(ks, address)
	if err != nil {
		utils.Fatalf("Could not list accounts: %v", err)
	}
	for trials := 0; trials < 3; trials++ {
		prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
		password := getPassPhrase(prompt, false, i, passwords)
		err = ks.Unlock(account, password)
		if err == nil {
			log.Info("Unlocked account", "address", account.Address.Hex())
			return account, password
		}
		if err, ok := err.(*keystore.AmbiguousAddrError); ok {
			log.Info("Unlocked account", "address", account.Address.Hex())
			return ambiguousAddrRecovery(ks, err, password), password
		}
		if err != keystore.ErrDecrypt {
			break
		}
	}
	utils.Fatalf("Failed to unlock account %s (%v)", address, err)

	return accounts.Account{}, ""
}

第一步通过给定的字符串地址获取一个账户对象。

func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) {
	if common.IsHexAddress(account) {
		return accounts.Account{Address: common.HexToAddress(account)}, nil
	}
	// Otherwise try to interpret the account as a keystore index
	index, err := strconv.Atoi(account)
	if err != nil || index < 0 {
		return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account)
	}
	log.Warn("-------------------------------------------------------------------")
	log.Warn("Referring to accounts by order in the keystore folder is dangerous!")
	log.Warn("This functionality is deprecated and will be removed in the future!")
	log.Warn("Please use explicit addresses! (can search via `geth account list`)")
	log.Warn("-------------------------------------------------------------------")

	accs := ks.Accounts()
	if len(accs) <= index {
		return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs))
	}
	return accs[index], nil
}

首先判断是否是16进制格式,是的话直接构造一个Account对象返回。否则的话我们指定的地址可能是地址的索引,所以先尝试转为整数,然后获取所有账户,取对应索引的账户返回即可。

回到unlockAccount中,接下来进行至多三次尝试,每次都是先利用getPassPhrase获取我们指定的密码,这个方法在前面创建新账户时提过。然后调用Unlock解锁账户。

func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
	return ks.TimedUnlock(a, passphrase, 0)
}

func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {
	a, key, err := ks.getDecryptedKey(a, passphrase)
	if err != nil {
		return err
	}

	ks.mu.Lock()
	defer ks.mu.Unlock()
	u, found := ks.unlocked[a.Address]
	if found {
		if u.abort == nil {
			zeroKey(key.PrivateKey)
			return nil
		}
		close(u.abort)
	}
	if timeout > 0 {
		u = &unlocked{Key: key, abort: make(chan struct{})}
		go ks.expire(a.Address, u, timeout)
	} else {
		u = &unlocked{Key: key}
	}
	ks.unlocked[a.Address] = u
	return nil
}

相关代码上面已经给出,Unlock中直接调用TimedUnlock方法,TimedUnlock中先用getDecryptedKey方法获取Key,这个在前面介绍新建账户是提到过,这里会得到一个Key对象,其中包含账户的地址及私钥。接着从unlocked中查找是否已解锁,unlocked中键就是账户地址,值是一个unlocked对象,其中存着账户的key。如果能从unlocked中找到对应地址的unlocked对象,则先判断unlocked对象的abort变量是否为空,是的话将私钥置为0,然后返回,否则关闭abort通道。如果找不到的话,先看参数中是否设置了timeout,是的话构造一个unlocked对象,之后执行expire,没有设置timeout时也只是简单构造一个unlocked对象,但不初始化其中的abort变量。最后将新建的unlocked对象放入unlocked中。expire的作用是设定一个定时器,在一段时间后将对应地址的unlocked对象从unlocked中删除,表示账户重新进入锁定状态。

回到unlockAccount中,如果解锁成功,则返回账户对象和密码。回到accountUpdate中,这时要求提供一个新密码,然后执行update方法

func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
	a, key, err := ks.getDecryptedKey(a, passphrase)
	if err != nil {
		return err
	}
	return ks.storage.StoreKey(a.URL.Path, key, newPassphrase)
}

相关逻辑都以分析过,只是先获取旧的Key然后用新的Key加密并保存密钥。

import

这个方法是导入一个密钥从而生成一个账户,他有4个flag前面都介绍过,这里不再介绍,他需要指定keyfile路径,执行的逻辑如下

func accountImport(ctx *cli.Context) error {
	keyfile := ctx.Args().First()
	if len(keyfile) == 0 {
		utils.Fatalf("keyfile must be given as argument")
	}
	key, err := crypto.LoadECDSA(keyfile)
	if err != nil {
		utils.Fatalf("Failed to load the private key: %v", err)
	}
	stack, _ := makeConfigNode(ctx)
	passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))

	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
	acct, err := ks.ImportECDSA(key, passphrase)
	if err != nil {
		utils.Fatalf("Could not create the account: %v", err)
	}
	fmt.Printf("Address: {%x}\n", acct.Address)
	return nil
}

首先从我们提供的文件中读取椭圆加密的私钥信息,然后构建一个节点,并取得我们给的密码,然后执行ImportECDSA

func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) {
	key := newKeyFromECDSA(priv)
	if ks.cache.hasAddress(key.Address) {
		return accounts.Account{}, fmt.Errorf("account already exists")
	}
	return ks.importKey(key, passphrase)
}

func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
	id := uuid.NewRandom()
	key := &Key{
		Id:         id,
		Address:    crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
		PrivateKey: privateKeyECDSA,
	}
	return key
}

func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
	a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
	if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
		return accounts.Account{}, err
	}
	ks.cache.add(a)
	ks.refreshWallets()
	return a, nil
}

获得私钥后先构建一个key,这里自然也就生成了地址。先检查该地址是否已存在,是的话报错,否则执行importKey。importKey就是直接创建一个账户对象然后保存密钥文件。

题图来自unsplash:https://unsplash.com/photos/lu5uhEAy2lI