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