go-ethereum 源码笔记(cmd 模块-geth 命令)

cmd 模块包含了很多子模块,基本上每个子模块表示一个可执行的命令,其中最重要的是 geth 命令,它是以太坊的命令行客户端。

geth 命令是以太坊提供的一个强大的命令行工具,它是使用以太坊的入口。它包括了很多子命令,你可以通过 geth --help 获得更多帮助信息。其运行方法是:geth [选项] 命令 [命令选项][参数…]

以下是 geth 包含的子命令以及对应的简单描述。

子命令 描述
account 管理账户
attach 启动交互式JavaScript环境(连接到节点)
bug 给 github 源代码仓库提 issue
console 启动交互式 JavaScript 环境
copydb 从文件夹创建本地链
dump 取出一个特定的区块
dumpconfig 显示配置值
export 导出区块链到文件
import 导入一个区块链文件
init 启动并初始化一个新的创世纪块
js 执行指定的JavaScript文件(多个)
license 显示许可信息
makecache 生成ethash验证缓存(用于测试)
makedag 生成ethash 挖矿DAG(用于测试)
monitor 监控和可视化节点指标
removedb 删除区块链和状态数据库
version 打印版本号
wallet 管理Ethereum预售钱包
help 显示一个命令或帮助一个命令列表

本文将逐一分析 geth 模块的源码,了解 geth 命令的实现原理。需要注意的是,这里我们不会深入分析每一个模块,因为这些模块的实现实际上是以太坊每个功能模块的实现,在后续的文章我们会一一分析。这里只分析 geth 命令的实现。

将涉及到 cmd, node 目录。

geth 的命令行是通过 github.com/urfave/cli 这个库实现的,通过这个库,我们可以轻松定义命令行程序的子命令,命令选项,命令参数,描述信息等等,如果想要进一步了解,可以查看该库文档。

geth 模块的入口在 cmd/geth/main.go 中,它会调用 urfave/cli 的中 app 的 run 方法,而 app 在 init 函数中初始化,在 Golang 中,如果有 init 方法,那么会在 main 函数之前执行 init 函数,它用于程序执行前的初始化工作。在 geth 模块中,init() 函数定义了命令行的入口是 geth,并且定义了 geth 的子命令、全局的命令选项、子命令的命令选项,按照 urfave/cli 的做法,不输入子命令会默认调用 geth,而 geth 方法其实就6行:

1
2
3
4
5
6
func geth(ctx *cli.Context) error {
node := makeFullNode(ctx)
startNode(ctx, node)
node.Wait()
return nil
}

它会调用 makeFullNode 函数初始化一个全节点,该方法在 geth/config.go 中,接着通过 startNode 函数启动一个全节点,以阻塞的方式运行,等待着节点被终止。

我们先深入到 makeFullNode 函数中。

1
2
3
4
5
6
7
8
9
10
func makeFullNode(ctx *cli.Context) *node.Node {
stack, cfg := makeConfigNode(ctx)
utils.RegisterEthService(stack, &cfg.Eth)
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
}
// whether enable whisper ...
// whether register eth stats ...
return stack
}

核心的逻辑是首先通过配置文件和 flag 生成系统级的配置,然后将服务注入到节点。
先说 makeConfigNode 方法。

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
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
cfg := gethConfig{
Eth: eth.DefaultConfig,
Shh: whisper.DefaultConfig,
Node: defaultNodeConfig(),
Dashboard: dashboard.DefaultConfig,
}

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

utils.SetNodeConfig(ctx, &cfg.Node)
stack, err := node.New(&cfg.Node)
if err != nil {
utils.Fatalf("Failed to create the protocol stack: %v", err)
}
utils.SetEthConfig(ctx, stack, &cfg.Eth)
if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
}

utils.SetShhConfig(ctx, stack, &cfg.Shh)
utils.SetDashboardConfig(ctx, &cfg.Dashboard)

return stack, cfg
}

makeConfigNode 会先载入默认配置,再载入配置文件中的配置,然后通过上下文的配置(在 cmd/geth/main.go 中的 init 方法中定义)进行设置。我们深入到 RegisterEthService 方法来查看服务是如何注入到节点中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
var err error
if cfg.SyncMode == downloader.LightSync {
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return les.New(ctx, cfg)
})
} else {
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
fullNode, err := eth.New(ctx, cfg)
if fullNode != nil && cfg.LightServ > 0 {
ls, _ := les.NewLesServer(fullNode, cfg)
fullNode.AddLesServer(ls)
}
return fullNode, err
})
}
if err != nil {
Fatalf("Failed to register the Ethereum service: %v", err)
}
}

RegisterEthService 的代码在 cmd/utils/flags.go 中,如果同步模式是轻量级同步模式,启动轻量级客户端,否则启动全节点,实际的注册方法是 stack.Register。注入服务其实就是将新的服务注入到 node 对象的 serviceFuncs 数组中。这些内容将go-ethereum 源码笔记(node 模块) 描述。

接下来我们继续看 geth/main.gostartNode 函数,看看如何启动节点,这个方法在 cmd/geth/main.go 中。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
func startNode(ctx *cli.Context, stack *node.Node) {
debug.Memsize.Add("node", stack)
utils.StartNode(stack)

ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)

passwords := utils.MakePasswordList(ctx)
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
for i, account := range unlocks {
if trimmed := strings.TrimSpace(account); trimmed != "" {
unlockAccount(ctx, ks, trimmed, i, passwords)
}
}
events := make(chan accounts.WalletEvent, 16)
stack.AccountManager().Subscribe(events)

go func() {
rpcClient, err := stack.Attach()
if err != nil {
utils.Fatalf("Failed to attach to self: %v", err)
}
stateReader := ethclient.NewClient(rpcClient)

for _, wallet := range stack.AccountManager().Wallets() {
if err := wallet.Open(""); err != nil {
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
}
}
for event := range events {
switch event.Kind {
case accounts.WalletArrived:
if err := event.Wallet.Open(""); err != nil {
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
}
case accounts.WalletOpened:
status, _ := event.Wallet.Status()
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)

if event.Wallet.URL().Scheme == "ledger" {
event.Wallet.SelfDerive(accounts.DefaultLedgerBaseDerivationPath, stateReader)
} else {
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
}

case accounts.WalletDropped:
log.Info("Old wallet dropped", "url", event.Wallet.URL())
event.Wallet.Close()
}
}
}()
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
if ctx.GlobalBool(utils.LightModeFlag.Name) || ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
utils.Fatalf("Light clients do not support mining")
}
var ethereum *eth.Ethereum
if err := stack.Service(&ethereum); err != nil {
utils.Fatalf("Ethereum service not running: %v", err)
}
if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
type threaded interface {
SetThreads(threads int)
}
if th, ok := ethereum.Engine().(threaded); ok {
th.SetThreads(threads)
}
}
ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
if err := ethereum.StartMining(true); err != nil {
utils.Fatalf("Failed to start mining: %v", err)
}
}
}

startNode 方法启动节点,会开启所有已经注册的协议,解锁请求的账户,开启 RPC/IPC 接口,并开始挖矿。这里我们不再深入。

举两个例子。想要查阅某个命令或某些参数的帮助信息,可以使用:

1
$ geth --help

查看各种命令,参数的提示信息。

如果想要启动一条私有链,可以使用下面的命令进行初始化:

1
$ geth  --datadir "./" init genesis.json

其中 genesis.json 是创始区块的配置信息。

然后执行下面的命令,创建私有链:

1
$ geth --datadir "./" --nodiscover console 2>>geth.log

上述就是直接运行 geth,不输入其他子命令的情况。geth 还有很多子命令,这些子命令在 init()app.Commands 赋值语句中可以看到,接下来会概述这些子命令。

chaincmd.go

initCommand: geth init

这个命令会进行初始化,生成创始区块。对应调用的方法是 initGenesis

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
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)
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
}

最终会调用 core/geth/chaincmdSetupGenesisBlock,这里具体的实现细节我们将在go-ethereum 源码笔记(core 模块-区块链操作) 介绍。

importCommand: geth import

导入一个区块链文件

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
func importChain(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
stack := makeFullNode(ctx)
chain, chainDb := utils.MakeChain(ctx, stack)
defer chainDb.Close()

var peakMemAlloc, peakMemSys uint64
go func() {
stats := new(runtime.MemStats)
for {
runtime.ReadMemStats(stats)
if atomic.LoadUint64(&peakMemAlloc) < stats.Alloc {
atomic.StoreUint64(&peakMemAlloc, stats.Alloc)
}
if atomic.LoadUint64(&peakMemSys) < stats.Sys {
atomic.StoreUint64(&peakMemSys, stats.Sys)
}
time.Sleep(5 * time.Second)
}
}()
start := time.Now()

if len(ctx.Args()) == 1 {
if err := utils.ImportChain(chain, ctx.Args().First()); err != nil {
log.Error("Import error", "err", err)
}
} else {
for _, arg := range ctx.Args() {
if err := utils.ImportChain(chain, arg); err != nil {
log.Error("Import error", "file", arg, "err", err)
}
}
}
chain.Stop()
fmt.Printf("Import done in %v.\n\n", time.Since(start))

db := chainDb.(*ethdb.LDBDatabase)

stats, err := db.LDB().GetProperty("leveldb.stats")
if err != nil {
utils.Fatalf("Failed to read database stats: %v", err)
}
fmt.Println(stats)

ioStats, err := db.LDB().GetProperty("leveldb.iostats")
if err != nil {
utils.Fatalf("Failed to read database iostats: %v", err)
}
fmt.Println(ioStats)

fmt.Printf("Trie cache misses: %d\n", trie.CacheMisses())
fmt.Printf("Trie cache unloads: %d\n\n", trie.CacheUnloads())

mem := new(runtime.MemStats)
runtime.ReadMemStats(mem)

fmt.Printf("Object memory: %.3f MB current, %.3f MB peak\n", float64(mem.Alloc)/1024/1024, float64(atomic.LoadUint64(&peakMemAlloc))/1024/1024)
fmt.Printf("System memory: %.3f MB current, %.3f MB peak\n", float64(mem.Sys)/1024/1024, float64(atomic.LoadUint64(&peakMemSys))/1024/1024)
fmt.Printf("Allocations: %.3f million\n", float64(mem.Mallocs)/1000000)
fmt.Printf("GC pause: %v\n\n", time.Duration(mem.PauseTotalNs))

if ctx.GlobalIsSet(utils.NoCompactionFlag.Name) {
return nil
}

start = time.Now()
fmt.Println("Compacting entire database...")
if err = db.LDB().CompactRange(util.Range{}); err != nil {
utils.Fatalf("Compaction failed: %v", err)
}
fmt.Printf("Compaction done in %v.\n\n", time.Since(start))

stats, err = db.LDB().GetProperty("leveldb.stats")
if err != nil {
utils.Fatalf("Failed to read database stats: %v", err)
}
fmt.Println(stats)

ioStats, err = db.LDB().GetProperty("leveldb.iostats")
if err != nil {
utils.Fatalf("Failed to read database iostats: %v", err)
}
fmt.Println(ioStats)

return nil
}

真正的逻辑在 utils/cmd.go 中的 ImportChain

exportCommand: geth export

导出一个区块链文件

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
func exportChain(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
stack := makeFullNode(ctx)
chain, _ := utils.MakeChain(ctx, stack)
start := time.Now()

var err error
fp := ctx.Args().First()
if len(ctx.Args()) < 3 {
err = utils.ExportChain(chain, fp)
} else {
first, ferr := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
last, lerr := strconv.ParseInt(ctx.Args().Get(2), 10, 64)
if ferr != nil || lerr != nil {
utils.Fatalf("Export error in parsing parameters: block number not an integer\n")
}
if first < 0 || last < 0 {
utils.Fatalf("Export error: block number must be greater than 0\n")
}
err = utils.ExportAppendChain(chain, fp, uint64(first), uint64(last))
}

if err != nil {
utils.Fatalf("Export error: %v\n", err)
}
fmt.Printf("Export done in %v\n", time.Since(start))
return nil
}

导出区块链的真正逻辑在 utils/cmd.go 中的 ExportChain 里。将导出一个 gz 文件。

importPreimagesCommand: geth import-preimages

将一个 preimages 导入当前节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func importPreimages(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
stack := makeFullNode(ctx)
diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)

start := time.Now()
if err := utils.ImportPreimages(diskdb, ctx.Args().First()); err != nil {
utils.Fatalf("Export error: %v\n", err)
}
fmt.Printf("Export done in %v\n", time.Since(start))
return nil
}

exportPreimagesCommand: geth export-preimages

从当前节点导出一个 image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func exportPreimages(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
stack := makeFullNode(ctx)
diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)

start := time.Now()
if err := utils.ExportPreimages(diskdb, ctx.Args().First()); err != nil {
utils.Fatalf("Export error: %v\n", err)
}
fmt.Printf("Export done in %v\n", time.Since(start))
return nil
}

copydbCommand: geth copydb

复制一个本地区块文件到文件夹

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
func copyDb(ctx *cli.Context) error {
if len(ctx.Args()) != 1 {
utils.Fatalf("Source chaindata directory path argument missing")
}
stack := makeFullNode(ctx)
chain, chainDb := utils.MakeChain(ctx, stack)

syncmode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode)
dl := downloader.New(syncmode, chainDb, new(event.TypeMux), chain, nil, nil)

db, err := ethdb.NewLDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name), 256)
if err != nil {
return err
}
hc, err := core.NewHeaderChain(db, chain.Config(), chain.Engine(), func() bool { return false })
if err != nil {
return err
}
peer := downloader.NewFakePeer("local", db, hc, dl)
if err = dl.RegisterPeer("local", 63, peer); err != nil {
return err
}
start := time.Now()

currentHeader := hc.CurrentHeader()
if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncmode); err != nil {
return err
}
for dl.Synchronising() {
time.Sleep(10 * time.Millisecond)
}
fmt.Printf("Database copy done in %v\n", time.Since(start))

start = time.Now()
fmt.Println("Compacting entire database...")
if err = chainDb.(*ethdb.LDBDatabase).LDB().CompactRange(util.Range{}); err != nil {
utils.Fatalf("Compaction failed: %v", err)
}
fmt.Printf("Compaction done in %v.\n\n", time.Since(start))

return nil
}

在一个文件夹中创建一个本地区块链。有意思的是这个过程并不是直接复制过去的,而是通过 downloader 模块里的 NewFakePeer 创建一个虚拟对等节点,然后再进行数据同步完成的。

removedbCommand: geth removedb

在当前数据库中移除区块链。

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
func removeDB(ctx *cli.Context) error {
stack, _ := makeConfigNode(ctx)

for _, name := range []string{"chaindata", "lightchaindata"} {
logger := log.New("database", name)

dbdir := stack.ResolvePath(name)
if !common.FileExist(dbdir) {
logger.Info("Database doesn't exist, skipping", "path", dbdir)
continue
}
fmt.Println(dbdir)
confirm, err := console.Stdin.PromptConfirm("Remove this database?")
switch {
case err != nil:
utils.Fatalf("%v", err)
case !confirm:
logger.Warn("Database deletion aborted")
default:
start := time.Now()
os.RemoveAll(dbdir)
logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start)))
}
}
return nil
}

删除数据库的过程倒是比较干脆,直接通过 os 模块移除这个文件夹。

dumpCommand: geth dump [<blockHash> | <blockNum>]...

dump 子命令可以移除一个或多个特定的区块

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
func dump(ctx *cli.Context) error {
stack := makeFullNode(ctx)
chain, chainDb := utils.MakeChain(ctx, stack)
for _, arg := range ctx.Args() {
var block *types.Block
if hashish(arg) {
block = chain.GetBlockByHash(common.HexToHash(arg))
} else {
num, _ := strconv.Atoi(arg)
block = chain.GetBlockByNumber(uint64(num))
}
if block == nil {
fmt.Println("{}")
utils.Fatalf("block not found")
} else {
state, err := state.New(block.Root(), state.NewDatabase(chainDb))
if err != nil {
utils.Fatalf("could not create new state: %v", err)
}
fmt.Printf("%s\n", state.Dump())
}
}
chainDb.Close()
return nil
}

先根据区块号获取区块,然后调用 state 的 Dump 移除即可,这部分的实现在之后go-ethereum 源码笔记(core 模块-区块链操作)会有描述。

monitorcmd.go

这部分代码不是核心内容,只是粗略的看了一下。

monitorComand: geth monitor

监控,图像化节点 metrics 数据

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
func monitor(ctx *cli.Context) error {
var (
client *rpc.Client
err error
)
endpoint := ctx.String(monitorCommandAttachFlag.Name)
if client, err = dialRPC(endpoint); err != nil {
utils.Fatalf("Unable to attach to geth node: %v", err)
}
defer client.Close()

metrics, err := retrieveMetrics(client)
if err != nil {
utils.Fatalf("Failed to retrieve system metrics: %v", err)
}
monitored := resolveMetrics(metrics, ctx.Args())
if len(monitored) == 0 {
list := expandMetrics(metrics, "")
sort.Strings(list)

if len(list) > 0 {
utils.Fatalf("No metrics specified.\n\nAvailable:\n - %s", strings.Join(list, "\n - "))
} else {
utils.Fatalf("No metrics collected by geth (--%s).\n", utils.MetricsEnabledFlag.Name)
}
}
sort.Strings(monitored)
if cols := len(monitored) / ctx.Int(monitorCommandRowsFlag.Name); cols > 6 {
utils.Fatalf("Requested metrics (%d) spans more that 6 columns:\n - %s", len(monitored), strings.Join(monitored, "\n - "))
}

if err := termui.Init(); err != nil {
utils.Fatalf("Unable to initialize terminal UI: %v", err)
}
defer termui.Close()

rows := len(monitored)
if max := ctx.Int(monitorCommandRowsFlag.Name); rows > max {
rows = max
}
cols := (len(monitored) + rows - 1) / rows
for i := 0; i < rows; i++ {
termui.Body.AddRows(termui.NewRow())
}

footer := termui.NewPar("")
footer.Block.Border = true
footer.Height = 3

charts := make([]*termui.LineChart, len(monitored))
units := make([]int, len(monitored))
data := make([][]float64, len(monitored))
for i := 0; i < len(monitored); i++ {
charts[i] = createChart((termui.TermHeight() - footer.Height) / rows)
row := termui.Body.Rows[i%rows]
row.Cols = append(row.Cols, termui.NewCol(12/cols, 0, charts[i]))
}
termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer)))

refreshCharts(client, monitored, data, units, charts, ctx, footer)
termui.Body.Align()
termui.Render(termui.Body)

termui.Handle("/sys/kbd/C-c", func(termui.Event) {
termui.StopLoop()
})
termui.Handle("/sys/wnd/resize", func(termui.Event) {
termui.Body.Width = termui.TermWidth()
for _, chart := range charts {
chart.Height = (termui.TermHeight() - footer.Height) / rows
}
termui.Body.Align()
termui.Render(termui.Body)
})
go func() {
tick := time.NewTicker(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second)
for range tick.C {
if refreshCharts(client, monitored, data, units, charts, ctx, footer) {
termui.Body.Align()
}
termui.Render(termui.Body)
}
}()
termui.Loop()
return nil
}

accountcmd.go

accountCommand

管理账户,这部分的实现在后续的博客go-ethereum 源码笔记(accounts, transaction 模块)有描述。

list: geth account list

1
2
3
4
5
6
7
8
9
10
11
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
}

通过调用 accounts/manager.goWallets 拿到所有账户。

new: geth account 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
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
}

创建一个账户,成功后输出地址。通过 accounts 模块实现。

update:geth account update <address>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}

先通过 AccountManager 拿到 keystore,然后调用 Update 更新密码

import: geth account import <keyfile>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

先通过 AccountManager 拿到 keystore,调用 ImportPreSaleKey 导入账户。

walletCommand: geth wallet import /path/to/my/presale.wallet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func importWallet(ctx *cli.Context) error {
keyfile := ctx.Args().First()
if len(keyfile) == 0 {
utils.Fatalf("keyfile must be given as argument")
}
keyJSON, err := ioutil.ReadFile(keyfile)
if err != nil {
utils.Fatalf("Could not read wallet file: %v", err)
}

stack, _ := makeConfigNode(ctx)
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))

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

通过 AccountManager 管理以太坊预售钱包。

consolecmd.go

consoleCommand: geth console

启动一个 Javascript 交互式环境

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
func localConsole(ctx *cli.Context) error {
node := makeFullNode(ctx)
startNode(ctx, node)
defer node.Stop()

client, err := node.Attach()
if err != nil {
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
if err != nil {
utils.Fatalf("Failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)

if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
console.Evaluate(script)
return nil
}
console.Welcome()
console.Interactive()

return nil
}

启动本地的一个交互式 Javascript 环境,功能是通过 console 模块提供的,而 console 模块是对 robertkrimen/otto 的一个封装。otto 是一个 Golang 实现的 Javascript 解释器,可以实现在 Golang 中执行 Javascript,并且可以让在虚拟机里的 Javascript 调用 Golang 函数,实现 Golang 和 Javascript 的相互操作。

attachCommand

启动一个 JS 交互式环境(连接到节点)

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
func remoteConsole(ctx *cli.Context) error {
endpoint := ctx.Args().First()
if endpoint == "" {
path := node.DefaultDataDir()
if ctx.GlobalIsSet(utils.DataDirFlag.Name) {
path = ctx.GlobalString(utils.DataDirFlag.Name)
}
if path != "" {
if ctx.GlobalBool(utils.TestnetFlag.Name) {
path = filepath.Join(path, "testnet")
} else if ctx.GlobalBool(utils.RinkebyFlag.Name) {
path = filepath.Join(path, "rinkeby")
}
}
endpoint = fmt.Sprintf("%s/geth.ipc", path)
}
client, err := dialRPC(endpoint)
if err != nil {
utils.Fatalf("Unable to attach to remote geth: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
if err != nil {
utils.Fatalf("Failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)

if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
console.Evaluate(script)
return nil
}

console.Welcome()
console.Interactive()

return nil
}

通过指定 endpoint 的方式,连接到某个节点的交互式 Javascript 环境。

javascriptCommand: geth js <jsfile> [jsfile...]

执行 Javascript 文件中的命令(可以为多个文件)

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
func ephemeralConsole(ctx *cli.Context) error {
node := makeFullNode(ctx)
startNode(ctx, node)
defer node.Stop()

client, err := node.Attach()
if err != nil {
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
if err != nil {
utils.Fatalf("Failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)
for _, file := range ctx.Args() {
if err = console.Execute(file); err != nil {
utils.Fatalf("Failed to execute %s: %v", file, err)
}
}
abort := make(chan os.Signal, 1)
signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)

go func() {
<-abort
os.Exit(0)
}()
console.Stop(true)

return nil
}

通过遍历调用传输的文件路径,执行 console.Execute,执行 js 命令。

misccmd.go

makecacheCommand: geth makecache <block number> <outputdir>

1
2
3
4
5
6
7
8
9
10
11
12
13
func makecache(ctx *cli.Context) error {
args := ctx.Args()
if len(args) != 2 {
utils.Fatalf(`Usage: geth makecache <block number> <outputdir>`)
}
block, err := strconv.ParseUint(args[0], 0, 64)
if err != nil {
utils.Fatalf("Invalid block number: %v", err)
}
ethash.MakeCache(block, args[1])

return nil
}

生成 ethash 的验证缓存。这部分内容将在go-ethereum 源码笔记(miner,consensus 模块)描述。

makedagCommand: geth makedag <block number> <outputdir>

1
2
3
4
5
6
7
8
9
10
11
12
13
func makedag(ctx *cli.Context) error {
args := ctx.Args()
if len(args) != 2 {
utils.Fatalf(`Usage: geth makedag <block number> <outputdir>`)
}
block, err := strconv.ParseUint(args[0], 0, 64)
if err != nil {
utils.Fatalf("Invalid block number: %v", err)
}
ethash.MakeDataset(block, args[1])

return nil
}

通过调用 ethash 的 MakeDataset,生成挖矿需要的 DAG 数据集。

versionCommand: geth version

输出版本号。

bugCommand

https://github.com/ethereum/go-ethereum/issues/new 这个 url 拼接参数,给源代码仓库提一个 issue

licenseCommand: geth license

输出 License 信息。

config.go

dumpConfigCommand: geth dumpconfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func dumpConfig(ctx *cli.Context) error {
_, cfg := makeConfigNode(ctx)
comment := ""

if cfg.Eth.Genesis != nil {
cfg.Eth.Genesis = nil
comment += "# Note: this config doesn't contain the genesis block.\n\n"
}

out, err := tomlSettings.Marshal(&cfg)
if err != nil {
return err
}
io.WriteString(os.Stdout, comment)
os.Stdout.Write(out)
return nil
}

makeConfigNode 前面已经提过,这个方法用来获取当前配置信息,dumpConfig 函数通过 makeConfigNode 获取配置,然后将其输出在屏幕。