精华内容
下载资源
问答
  • 最新版以太坊源码

    2018-09-07 10:20:28
    最新版以太坊源码
  • 以太坊源码 区块链

    2018-08-16 21:22:52
    以太坊作为目前区块链技术2.0的代表作品,无论是它独创的智能合约以及它本身交易的速度都优于bitcoin,
  • 本书用来描述以太坊网络发现的实现原理。很适合相关从事和学习以太坊技术的同学。
  • 以太坊作1.9.19版本源码,它独创的智能合约以及它本身交易的速度都优于bitcoin。欢迎下载...
  • 以太坊源码架构

    2018-05-03 20:52:05
    该文档是描述以太坊源码架构的一本典型资料,能为读者提供一个很好的导读。
  • go-ethereum之p2p节点发现原理解析,节省分析代码的时间,一目了然。
  • 一共大概20篇文章,适合智能合约开发工程师,区块链开发工程师,底层公链开发工程师 阅读,希望可以帮助到你们,如果发现哪里有不对的 地方,希望可以给出指正。
  • 根据硬分叉改变一些stateDB的状态(由于以太坊历史上有一些硬分叉的过程,所有现在的源码很多位置都有对于硬分叉的判断,本系列源码分析只关注主流程,以后不再赘述有关硬分叉的部分) 循环执行区块中的交易,这一...

    一、 典型交易流程源码

    在这里插入图片描述
    入口在StateProcessor的Process()函数,具体为什么在这里,可以从下一节中的挖矿源码分析中看到。

    1. 根据硬分叉改变一些stateDB的状态(由于以太坊历史上有一些硬分叉的过程,所有现在的源码很多位置都有对于硬分叉的判断,本系列源码分析只关注主流程,以后不再赘述有关硬分叉的部分)
    2. 循环执行区块中的交易,这一部分是整个交易流程的核心部分,所有对交易的验证、奖励、gas的计算、evm的执行都在这部分,主要分为以下几部分:
    • a) state初始设置,如交易hash、块hash等已有信息。

    • b) 执行交易,返回receipt和logs。这里返回的receipt是虚拟机执行的每一步的结果,logs是对应的日志,返回这两个并形成merkle根是为了快速验证每一步的结果都是正确的。
      * statedb.Prepare(tx.Hash(), block.Hash(), i)函数做一些准备工作,包括创建evm虚拟环境等;
      * 由ApplyTransaction()函数进行具体的交易执行(包括转账和智能合约的相关动作),交付gas,这里的gas包括支付给矿工的gas和refundGas;
      * 更新merkle根。

      在这个部分中,第一次出现了merkle树的概念,在这里的作用是把每一个小的hash由树的形式最终计算出一个merkle root,任何一个小的hash的改变都将导致merkle root的改变,这样我们在区块链中就可以通过merkle root快速验证区块中的数据是否被篡改。
      Gas在以太坊中的作用相信大家已经都了解了,但是这里还出现了一个refundGas,根据源码来看,这笔是返回给发起交易的账户的,具体为什么要这样做还不清楚,这部分在以太坊黄皮书中有讲述(计算方式),感兴趣的读者可以自己去看一下https://ethereum.stackexchange.com/questions/594/what-are-the-limits-to-gas-refunds。

    1. 最后是调用共识中的Finalize函数,这个函数主要是完成一些对区块挖掘者的奖励等最后收尾工作,值得注意的是这个函数中的操作一般不会写入merkle根,这也是为什么每个矿工挖出来的区块都奖励了自己(和其他矿工生成的区块有一笔交易不同),也可以通过其他矿工的验证。

    二、挖矿

    在这里插入图片描述
    挖矿的源码包在miner目录下,其中stress_clique.go和stress_ethash.go都是测试共识的代码,其余几个才是主要挖矿的代码。Miner对象核心就是worker对象,Worker对象主要用于准备当前区块环境,比如这个区块包含的交易等。然后调用共识引擎挖矿,以pow为例,主要就是算随机数然后填充到当前区块中,完成挖掘。我们从miner.go开始看起。

    1. 这个部分主要是构造一个miner对象,这部分主要是构造worker对象、监听相关事件、启动相关的一些线程,接下来我们具体来看。
    • a) 构造worker对象以及导入相关配置(如chainConfig),worker对象主要是准备区块,所以在worker对象中会订阅跟交易相关的事件,这里订阅了三个事件:
      • eth.TxPool().SubscribeNewTxsEvent,这个是TxPool对象发出的,指的是一个新的交易tx被加入了TxPool,这时如果worker没有处于挖掘中,那么就去执行这个tx,并把它收纳进Work.txs数组,为下次挖掘新区块备用。
      • eth.BlockChain().SubscribeChainHeadEvent, ChainHeadEvent是指区块链中已经加入了一个新的区块作为整个链的链头,这时worker的回应是立即开始准备挖掘下一个新区块
      • eth.BlockChain().SubscribeChainSideEvent ChainSideEvent指区块链中加入了一个新区块作为当前链头的旁支,worker会把这个区块收纳进possibleUncles[]数组,作为下一个挖掘新区块可能的Uncle之一
    • b) 启动线程work.mainLoop、work.newWorkLoop、worker.resultLoop、worker.taskLoop。
      • i. work.mainLoop
        挖矿主循环,这里面又会监听几个事件,newWorkCh代表一个新的区块需要提交挖矿;chainSideCh代表发现了一个叔区块,我们需要视情况而定加入到当前区块中,重新开始挖矿(一个区块最多包含两个叔区块);txsCh代表接收到一个新的交易,如果没有开始挖掘,则执行这个交易,加入当前区块中(上一部分中交易执行的入口就是从这里发现的)。
      • ii. work.newWorkLoop
        这里主要是work主线程,负责提交任务。这里主要涉及到三个比较重要的函数,分别是commit()、recalcRecommit()、clearPending()。Commit用于提交任务,发送newWorkReq到上一步中的newWorkCh通;,recalcRecommit是根据反馈计算一个等待时间;clearPending是清除当前在等待的任务。搞清楚这几个函数的作用之后,这里面监听的事件就很容易明白了,概括一下就是要么等待,要么就是清除队列准备提交(相信读者经过上面的学习这里应该很容易看懂,篇幅有限,笔者不再赘述)。
      • iii. worker.resultLoop
        结果处理,用于处理封装好之后的区块。通过resultCh接收到封装好的区块之后,做一个简单的验证(可能由于resubmit导致区块重复),构造好logs和event,发送相关事件。最后将这个块插入到unconfirmed链中。
        注意,这里第一次出现了unconfirmed这个概念,这相当于未确认的块,所有挖出来或者接收的区块都会先插入到这个地方,后续遇到这个我们再详细讲。
        iv. worker.taskLoop
        这是负责连接worker和agent的地方,newWorkLoop中的commit函数会发送taskCh到taskLoop中,由taskLoop调用agent进行seal。也就是说,taskLoop负责把worker的任务推送到具体的engine。
    1. 经过上述过程,整个worker对象已经运行起来了,miner的核心就是这个worker,接下来就是启动miner.update()。这个函数首先订阅了downloader的相关的几个事件。收到Downloader的StartEvent时,意味者此时本节点正在从其他节点下载新区块,这时miner会立即停止进行中的挖掘工作,并继续监听;如果收到DoneEvent或FailEvent时,意味本节点的下载任务已结束-无论下载成功或失败-此时都可以开始挖掘新区块,并且此时会退出Downloader事件的监听。也就是说,只要接收到来自其他节点的区块,本节点就会自动停止当前区块的挖掘,转向下一个区块。

    三、 总结

    这一节我们从典型的交易流程讲起,从而拓展到挖矿的过程。其实以太坊中最核心的就是挖矿和交易的处理,个人认为如果能自己把这个流程梳理清楚了,其他的部分也就不在话下。以太坊其他的部分,比如rpc、event、downloader、p2p等等,其实都是在为这整个过程做服务。其他的模块我们多多少少都能在这一次的源码分析中看见,这一部分也是区块链创新的核心所在。
    接下来是我对以太坊的一些感悟:

    • Gas是以太坊的灵魂,正是由了这个部分,整个系统才作为一个完善的系统一直在运行。
    • 以太坊另一个大的创新就是EVM,这正是区别于比特币的东西,也正是这个把区块链又往前推动了一大步,让区块链处理业务变成了可能,包括后来的fabric、BCOS、EOS等系统都保留了虚拟机这个东西。
    • 为了保证分叉的减少,以太坊另一个创新在于引入了叔区块的概念,不能被引入主链的区块有机会作为叔区块存在。这样大大减少了孤块的产生,减少了分叉,提升了整个网络的安全性。值得注意的是叔区块也是有一定奖励的(感兴趣的读者可以自行搜索,不再赘述)。
    • 等待区块确认这个概念是用unconfirmed chain实现的,通过这个集合的移动,我们明白了区块确认的原理(笔者以前一直以为未确认只是业界共识,没有在代码中实现)。

    以下是我读源码的一些收获:

    • 在整个系统中event是一个很重要的存在,我们发现事件的订阅和退订似乎无处不在。整个系统用event串联了起来,我觉得有必要去仔细学习以下以太坊的event的使用。
    • Miner过程中大量的使用了协程,我想这也是大型项目使用go语言的原因吧。每个协程都各自为战,然后由通道将不同协程串联起来。这样完成了整个系统的解耦,也使得整个系统有更好的可扩展性(我觉得这一点值得我们好好学习,建议读者去看Go并发编程实战这本书),我觉得mainLoop、newWorkLoop、resultLoop、taskLoop这几个协程的协作是一个很好的例子。
    • 跟上一篇博客结论相同,这次miner源码分析中,我们发现了整个过程只是在构建一个miner对象,同时启动了一些协程。源码过程核心就是构造需要的对象(miner),在这个过程中启动工作协程。

    四、参考文献

    https://github.com/ethereum/go-ethereum 以太坊源码go-ethereum
    https://blog.csdn.net/KeenCryp/article/details/86586132 refundGas说明
    https://blog.csdn.net/teaspring/article/details/78050274 以太坊源码分析
    https://github.com/ZtesoftCS/go-ethereum-code-analysis/blob/master/miner挖矿部分源码分析CPU挖矿.md 以太坊源码分析
    https://blog.csdn.net/u012412689/article/details/88534757 本系列上一篇博客

    其他:笔者水平有限,如有错误请联系M201672845@hust.edu.cn

    展开全文
  • 以太坊源码分析报告

    2018-11-08 09:03:07
    以太坊go-ethereum的源码分析报告总结, 包括启动流程, p2p网络, 交易池, 挖矿, EVM 及同步
  • 以太坊(英语:Ethereum)是一个开源的有智能合约功能的公共区块链平台。通过其专用加密货币以太币(Ether,又称“以太币”)提供去中心化的虚拟机,这是其最新的源代码,是go语言版本。
  • 以太坊源码全解读

    千次阅读 2020-02-01 10:03:32
    https://github.com/Billy1900/Ethereum-tutoria 链接进入即可阅读源码解析,望大家多多批评

    https://github.com/Billy1900/Ethereum-tutorial

    链接进入即可阅读源码解析,望大家多多批评

    展开全文
  • 以太坊源码分析-以太坊启动

    万次阅读 2018-01-25 11:30:40
    以太坊源码分析-开篇13年时第一次知道比特币的存在,那时仅仅是跟风炒币而已,没有具体去了解相关的技术细节。前不久无意中有人提到「比特币在没有大维护的情况下完美运行8年了」,瞬间惊呆了,于是乎开始了解区块链...

    以太坊源码分析-开篇13年时第一次知道比特币的存在,那时仅仅是跟风炒币而已,没有具体去了解相关的技术细节。前不久无意中有人提到「比特币在没有大维护的情况下完美运行8年了」,瞬间惊呆了,于是乎开始了解区块链的相关知识。在区块链中,我选择以太坊作为切入点,分析以太坊的相关底层技术细节实现。由于初学不久,分析不对的地方烦请指出。

    以太坊开发环境搭建

    1. 安装Go环境,请自行Google
    2. 从github上clone以太坊的Go语言版本go-ethereum
    3. 命令行直接进入go-ethereum目录,执行「make all」命令,等待编译完成后,在build目录中将生成一个bin目录,该目录中就是我们编译好的相关命令。如下: 58005960-23EA-4C5E-B207-AC2DB664C471.png下面我们来简单的介绍这几个命令的使用场景
    • abigen-- 一个源代码生成器,它将Ethereum智能合约定义(代码) 转换为易于使用的、编译时类型安全的Go package。 如果合约字节码也available的话,它可以在普通的Ethereum智能合约ABI上扩展功能。 同时也能编译Solidity源文件,使开发更加精简。
    • bootnod–此Ethereum客户端实现的剥离版本只参与 网络节点发现 协议,但不运行任何更高级别的应用协议。 它可以用作轻量级引导节点,以帮助在私有网络中查找peers。
    • evm-- 能够在可配置环境和执行模式下运行字节码片段的Developer utility版本的的EVM(Ethereum Virtual Machine)。 其目的是允许对EVM操作码进行封装,细粒度的调试。
    • faucet–暂时不知道其使用场景,其help没有相关的解释,后续看下源码再来补充。
    • geth–主要Ethereum CLI客户端。它是Ethereum网络(以太坊主网,测试网络或私有网)的入口点,使用此命令可以使节点作为full node(默认),或者archive node(保留所有历史状态)或light node(检索数据实时)运行。 其他进程可以通过暴露在HTTP,WebSocket和/或IPC传输之上的JSON RPC端点作为通向Ethereum网络的网关使用。
    • puppeth–暂时不知道其含义,后续补充。
    • rlpdump–开发者通用工具,用来把二进制RLP (Recursive Length Prefix) (Ethereum 协议中用于网络及一致性的数据编码) 转换成用户友好的分层表示。
    • swarm–swarm守护进程和工具,这是swarm网络的进入点。

    接下来我们从以太坊最重要的geth命令入手,来分析以太坊的启动流程。首先,在命令行输入一下命令打开控制台:

    build/bin/geth --datadir=./dev/data0 --networkid 1 console

    接下来我们来看启动入口main函数,它位于/cmd/geth/main.go文件中,main函数的初始化函数代码如下:

    func init() {
    	// Initialize the CLI app and start Geth
    	app.Action = geth
    	app.HideVersion = true // we have a command to print the version
    	app.Copyright = "Copyright 2013-2017 The go-ethereum Authors"
    	app.Commands = []cli.Command{
    		// See chaincmd.go:
    		initCommand,
    		importCommand,
    		exportCommand,
    		removedbCommand,
    		dumpCommand,
    		// See monitorcmd.go:
    		monitorCommand,
    		// See accountcmd.go:
    		accountCommand,
    		walletCommand,
    		// See consolecmd.go:
    		consoleCommand,
    		attachCommand,
    		javascriptCommand,
    		// See misccmd.go:
    		makedagCommand,
    		versionCommand,
    		bugCommand,
    		licenseCommand,
    		// See config.go
    		dumpConfigCommand,
    	}
    	app.Flags = append(app.Flags, nodeFlags...)
    	app.Flags = append(app.Flags, rpcFlags...)
    	app.Flags = append(app.Flags, consoleFlags...)
    	app.Flags = append(app.Flags, debug.Flags...)
    	app.Flags = append(app.Flags, whisperFlags...)
    	app.Before = func(ctx *cli.Context) error {
    		runtime.GOMAXPROCS(runtime.NumCPU())
    		if err := debug.Setup(ctx); err != nil {
    			return err
    		}
    		// Start system runtime metrics collection
    		go metrics.CollectProcessMetrics(3 * time.Second)
    		utils.SetupNetwork(ctx)
    		return nil
    	}
    	app.After = func(ctx *cli.Context) error {
    		debug.Exit()
    		console.Stdin.Close() // Resets terminal mode.
    		return nil
    	}
    }

    init函数主要是做了一些初始化的工作,其中比较重要的有三个地方,app.Action=geth,app.Commands中consoleCommand,以及App.Before指向的匿名函数,后续使用到的时候我们再来分析。我们再来看看main函数:

    func main() {
    	if err := app.Run(os.Args); err != nil {
    		fmt.Fprintln(os.Stderr, err)
    		os.Exit(1)
    	}
    }

    main函数的实现很简单,仅仅调用了app.Run函数,如果调用异常,则退出。接下里我们看看app.Run函数的逻辑处理

    func (a *App) Run(arguments []string) (err error) {
    	a.Setup()
    
    	// handle the completion flag separately from the flagset since
    	// completion could be attempted after a flag, but before its value was put
    	// on the command line. this causes the flagset to interpret the completion
    	// flag name as the value of the flag before it which is undesirable
    	// note that we can only do this because the shell autocomplete function
    	// always appends the completion flag at the end of the command
    	shellComplete, arguments := checkShellCompleteFlag(a, arguments)
    
    	// parse flags
    	set, err := flagSet(a.Name, a.Flags)
    	if err != nil {
    		return err
    	}
    
    	set.SetOutput(ioutil.Discard)
    	err = set.Parse(arguments[1:])
    	nerr := normalizeFlags(a.Flags, set)
    	context := NewContext(a, set, nil)
    	if nerr != nil {
    		fmt.Fprintln(a.Writer, nerr)
    		ShowAppHelp(context)
    		return nerr
    	}
    	context.shellComplete = shellComplete
    
    	if checkCompletions(context) {
    		return nil
    	}
    
    	if err != nil {
    		if a.OnUsageError != nil {
    			err := a.OnUsageError(context, err, false)
    			HandleExitCoder(err)
    			return err
    		}
    		fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
    		ShowAppHelp(context)
    		return err
    	}
    
    	if !a.HideHelp && checkHelp(context) {
    		ShowAppHelp(context)
    		return nil
    	}
    
    	if !a.HideVersion && checkVersion(context) {
    		ShowVersion(context)
    		return nil
    	}
    
    	if a.After != nil {
    		defer func() {
    			if afterErr := a.After(context); afterErr != nil {
    				if err != nil {
    					err = NewMultiError(err, afterErr)
    				} else {
    					err = afterErr
    				}
    			}
    		}()
    	}
    
    	if a.Before != nil {
    		beforeErr := a.Before(context)
    		if beforeErr != nil {
    			fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
    			ShowAppHelp(context)
    			HandleExitCoder(beforeErr)
    			err = beforeErr
    			return err
    		}
    	}
    
    	args := context.Args()
    	if args.Present() {
    		name := args.First()
    		c := a.Command(name)
    		if c != nil {
    			return c.Run(context)
    		}
    	}
    
    	if a.Action == nil {
    		a.Action = helpCommand.Action
    	}
    
    	// Run default Action
    	err = HandleAction(a.Action, context)
    
    	HandleExitCoder(err)
    	return err
    }

    a.Setup仅仅是做了些简单的处理,比如相关的Auther、Email、重新创建Command切片等等.接下来我们看看下面的这个if判断

    if args.Present() {
    		name := args.First()
    		c := a.Command(name)
    		if c != nil {
    			return c.Run(context)
    		}
    	}

    由于我们前面在控制台输入的命令(build/bin/geth --datadir=./dev/data0 --networkid 1 console)长度不为0,因此执行

    c.Run(context)

    操作,此时的命令其实就是我们的console命令。接下来我们看看Run方法,Run方法的代码如下:

    func (c Command) Run(ctx *Context) (err error) {
    	if len(c.Subcommands) > 0 {
    		return c.startApp(ctx)
    	}
    
    	if !c.HideHelp && (HelpFlag != BoolFlag{}) {
    		// append help to flags
    		c.Flags = append(
    			c.Flags,
    			HelpFlag,
    		)
    	}
    
    	set, err := flagSet(c.Name, c.Flags)
    	if err != nil {
    		return err
    	}
    	set.SetOutput(ioutil.Discard)
    
    	if c.SkipFlagParsing {
    		err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
    	} else if !c.SkipArgReorder {
    		firstFlagIndex := -1
    		terminatorIndex := -1
    		for index, arg := range ctx.Args() {
    			if arg == "--" {
    				terminatorIndex = index
    				break
    			} else if arg == "-" {
    				// Do nothing. A dash alone is not really a flag.
    				continue
    			} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
    				firstFlagIndex = index
    			}
    		}
    
    		if firstFlagIndex > -1 {
    			args := ctx.Args()
    			regularArgs := make([]string, len(args[1:firstFlagIndex]))
    			copy(regularArgs, args[1:firstFlagIndex])
    
    			var flagArgs []string
    			if terminatorIndex > -1 {
    				flagArgs = args[firstFlagIndex:terminatorIndex]
    				regularArgs = append(regularArgs, args[terminatorIndex:]...)
    			} else {
    				flagArgs = args[firstFlagIndex:]
    			}
    
    			err = set.Parse(append(flagArgs, regularArgs...))
    		} else {
    			err = set.Parse(ctx.Args().Tail())
    		}
    	} else {
    		err = set.Parse(ctx.Args().Tail())
    	}
    
    	nerr := normalizeFlags(c.Flags, set)
    	if nerr != nil {
    		fmt.Fprintln(ctx.App.Writer, nerr)
    		fmt.Fprintln(ctx.App.Writer)
    		ShowCommandHelp(ctx, c.Name)
    		return nerr
    	}
    
    	context := NewContext(ctx.App, set, ctx)
    	if checkCommandCompletions(context, c.Name) {
    		return nil
    	}
    
    	if err != nil {
    		if c.OnUsageError != nil {
    			err := c.OnUsageError(ctx, err, false)
    			HandleExitCoder(err)
    			return err
    		}
    		fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error())
    		fmt.Fprintln(ctx.App.Writer)
    		ShowCommandHelp(ctx, c.Name)
    		return err
    	}
    
    	if checkCommandHelp(context, c.Name) {
    		return nil
    	}
    
    	if c.After != nil {
    		defer func() {
    			afterErr := c.After(context)
    			if afterErr != nil {
    				HandleExitCoder(err)
    				if err != nil {
    					err = NewMultiError(err, afterErr)
    				} else {
    					err = afterErr
    				}
    			}
    		}()
    	}
    
    	if c.Before != nil {
    		err = c.Before(context)
    		if err != nil {
    			fmt.Fprintln(ctx.App.Writer, err)
    			fmt.Fprintln(ctx.App.Writer)
    			ShowCommandHelp(ctx, c.Name)
    			HandleExitCoder(err)
    			return err
    		}
    	}
    
    	if c.Action == nil {
    		c.Action = helpSubcommand.Action
    	}
    
    	context.Command = c
    	err = HandleAction(c.Action, context)
    
    	if err != nil {
    		HandleExitCoder(err)
    	}
    	return err
    }

    该主要是设置flag、解析输入的命令行参数、创建全局的context、将当前命令保存到全局context中,接下来调用HandleAction来处理命令,HandleAction的函数实现如下:

    func HandleAction(action interface{}, context *Context) (err error) {
    	if a, ok := action.(ActionFunc); ok {
    		return a(context)
    	} else if a, ok := action.(func(*Context) error); ok {
    		return a(context)
    	} else if a, ok := action.(func(*Context)); ok { // deprecated function signature
    		a(context)
    		return nil
    	} else {
    		return errInvalidActionType
    	}
    }

    action的类型是「func(*Context) error」,此时将执行a(context)方法,那么此时调用那个Action呢,答案就是我们前面提到的App.init()初始化命令时的consoleCommand,接下来我们来看看cmd/geth/consolecmd中的consoleCommand:

    consoleCommand = cli.Command{
    		Action:   utils.MigrateFlags(localConsole),
    		Name:     "console",
    		Usage:    "Start an interactive JavaScript environment",
    		Flags:    append(append(append(nodeFlags, rpcFlags...), consoleFlags...), whisperFlags...),
    		Category: "CONSOLE COMMANDS",
    		Description: `
    The Geth console is an interactive shell for the JavaScript runtime environment
    which exposes a node admin interface as well as the Ðapp JavaScript API.
    See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.`,
    	}

    其Action中的localConsole函数如下:

    func localConsole(ctx *cli.Context) error {
    	// Create and start the node based on the CLI flags
    	node := makeFullNode(ctx)
    	startNode(ctx, node)
    	defer node.Stop()
    
    	// Attach to the newly started node and start the JavaScript console
    	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 only a short execution was requested, evaluate and return
    	if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
    		console.Evaluate(script)
    		return nil
    	}
    	// Otherwise print the welcome screen and enter interactive mode
    	console.Welcome()
    	console.Interactive()
    
    	return nil
    }
    

    该函数非常重要,主要完成以下几件事情

    1. 首先会创建一个节点、同时启动该节点
    2. 创建一个console的实例
    3. 显示Welcome信息
    4. 创建一个无限循环用于在控制台交互

    首先我们来看该Node是如何创建的。makeFullNode函数的实现如下:

    func makeFullNode(ctx *cli.Context) *node.Node {
    	stack, cfg := makeConfigNode(ctx)
    
    	utils.RegisterEthService(stack, &cfg.Eth)
    
    	// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
    	shhEnabled := enableWhisper(ctx)
    	shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)
    	if shhEnabled || shhAutoEnabled {
    		if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
    			cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
    		}
    		if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
    			cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
    		}
    		utils.RegisterShhService(stack, &cfg.Shh)
    	}
    
    	// Add the Ethereum Stats daemon if requested.
    	if cfg.Ethstats.URL != "" {
    		utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
    	}
    
    	// Add the release oracle service so it boots along with node.
    	if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
    		config := release.Config{
    			Oracle: relOracle,
    			Major:  uint32(params.VersionMajor),
    			Minor:  uint32(params.VersionMinor),
    			Patch:  uint32(params.VersionPatch),
    		}
    		commit, _ := hex.DecodeString(gitCommit)
    		copy(config.Commit[:], commit)
    		return release.NewReleaseService(ctx, config)
    	}); err != nil {
    		utils.Fatalf("Failed to register the Geth release oracle service: %v", err)
    	}
    	return stack
    }

    该函数首先创建一个Node,然后注册一个Ethereum Service,我们继续分析Node是如何创建的,makeConfigNode的函数实现逻辑如下:

    func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
    	// Load defaults.
    	cfg := gethConfig{
    		Eth:  eth.DefaultConfig,
    		Shh:  whisper.DefaultConfig,
    		Node: defaultNodeConfig(),
    	}
    
    	// Load config file.
    	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
    		if err := loadConfig(file, &cfg); err != nil {
    			utils.Fatalf("%v", err)
    		}
    	}
    
    	// Apply flags.
    	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)
    
    	return stack, cfg
    }

    首先初始化gethConfig的变量,接下来调用node.New方法来创建一个Node,我们来看看New函数的实现:

    func New(conf *Config) (*Node, error) {
    	// Copy config and resolve the datadir so future changes to the current
    	// working directory don't affect the node.
    	confCopy := *conf
    	conf = &confCopy
    	if conf.DataDir != "" {
    		absdatadir, err := filepath.Abs(conf.DataDir)
    		if err != nil {
    			return nil, err
    		}
    		conf.DataDir = absdatadir
    	}
    	// Ensure that the instance name doesn't cause weird conflicts with
    	// other files in the data directory.
    	if strings.ContainsAny(conf.Name, `/\`) {
    		return nil, errors.New(`Config.Name must not contain '/' or '\'`)
    	}
    	if conf.Name == datadirDefaultKeyStore {
    		return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
    	}
    	if strings.HasSuffix(conf.Name, ".ipc") {
    		return nil, errors.New(`Config.Name cannot end in ".ipc"`)
    	}
    	// Ensure that the AccountManager method works before the node has started.
    	// We rely on this in cmd/geth.
    	am, ephemeralKeystore, err := makeAccountManager(conf)
    	if err != nil {
    		return nil, err
    	}
    	// Note: any interaction with Config that would create/touch files
    	// in the data directory or instance directory is delayed until Start.
    	return &Node{
    		accman:            am,
    		ephemeralKeystore: ephemeralKeystore,
    		config:            conf,
    		serviceFuncs:      []ServiceConstructor{},
    		ipcEndpoint:       conf.IPCEndpoint(),
    		httpEndpoint:      conf.HTTPEndpoint(),
    		wsEndpoint:        conf.WSEndpoint(),
    		eventmux:          new(event.TypeMux),
    	}, nil
    }

    New函数首先将保存数据的目录转换成绝对路径,然后对配置进行相关的验证,确认是否合法,接下来创建一个AccountManager实例,然后创建一个Node节点,总体来说其内部的逻辑比较的简单。由于AccountManager负责Account的相关管理操作且后续经常使用,接下来我们看看是AccountManager的具体逻辑实现。AccountManager位于/node/config.go文件中,其实现如下:

    func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
    	scryptN := keystore.StandardScryptN
    	scryptP := keystore.StandardScryptP
    	if conf.UseLightweightKDF {
    		scryptN = keystore.LightScryptN
    		scryptP = keystore.LightScryptP
    	}
    
    	var (
    		keydir    string
    		ephemeral string
    		err       error
    	)
    	switch {
    	case filepath.IsAbs(conf.KeyStoreDir):
    		keydir = conf.KeyStoreDir
    	case conf.DataDir != "":
    		if conf.KeyStoreDir == "" {
    			keydir = filepath.Join(conf.DataDir, datadirDefaultKeyStore)
    		} else {
    			keydir, err = filepath.Abs(conf.KeyStoreDir)
    		}
    	case conf.KeyStoreDir != "":
    		keydir, err = filepath.Abs(conf.KeyStoreDir)
    	default:
    		// There is no datadir.
    		keydir, err = ioutil.TempDir("", "go-ethereum-keystore")
    		ephemeral = keydir
    	}
    	if err != nil {
    		return nil, "", err
    	}
    	if err := os.MkdirAll(keydir, 0700); err != nil {
    		return nil, "", err
    	}
    	// Assemble the account manager and supported backends
    	backends := []accounts.Backend{
    		keystore.NewKeyStore(keydir, scryptN, scryptP),
    	}
    	if !conf.NoUSB {
    		if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
    			log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
    		} else {
    			backends = append(backends, ledgerhub)
    		}
    	}
    	return accounts.NewManager(backends...), ephemeral, nil
    }

    由于在上面,我们已经将保存数据的目录转换成了绝对路径(由于在命令行,我们已经指定了datadir且存在,如果不存在将创建一个默认的go-ethereum-keystore来保存相关信息),接下来我们来看keystore.NewKeyStore函数。该函数位于/accounts/keystore/keystore.go文件中,其实现如下:

    func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
    	keydir, _ = filepath.Abs(keydir)
    	ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
    	ks.init(keydir)
    	return ks
    }

    首先转换成绝对路径(前面已经转换,应该没有必要再次检测),接下来我们看看

    ks.init(keydir)

    方法的实现,其实现如下:

    
    func (ks *KeyStore) init(keydir string) {
    	// Lock the mutex since the account cache might call back with events
    	ks.mu.Lock()
    	defer ks.mu.Unlock()
    
    	// Initialize the set of unlocked keys and the account cache
    	ks.unlocked = make(map[common.Address]*unlocked)
    	ks.cache, ks.changes = newAccountCache(keydir)
    
    	// TODO: In order for this finalizer to work, there must be no references
    	// to ks. addressCache doesn't keep a reference but unlocked keys do,
    	// so the finalizer will not trigger until all timed unlocks have expired.
    	runtime.SetFinalizer(ks, func(m *KeyStore) {
    		m.cache.close()
    	})
    	// Create the initial list of wallets from the cache
    	accs := ks.cache.accounts()
    	ks.wallets = make([]accounts.Wallet, len(accs))
    	for i := 0; i < len(accs); i++ {
    		ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks}
    	}
    }

    该方法首先从缓存中(如果缓存存在则直接获取,否则从datadir中读取)获取Account,然后保存在keystore的钱包中,我们主要来看看ks.cache.accounts()函数,其实现逻辑如下:

    func (ac *accountCache) accounts() []accounts.Account {
    	ac.maybeReload()
    	ac.mu.Lock()
    	defer ac.mu.Unlock()
    	cpy := make([]accounts.Account, len(ac.all))
    	copy(cpy, ac.all)
    	return cpy
    }

    如果账户数据未被加载到内存中,则首先加载进来,然后copy一份,防止外面对缓存的账户做修改,我们来看看ac.maybeReload是如何加载账户信息的。其实现如下:

    func (ac *accountCache) maybeReload() {
    	ac.mu.Lock()
    	defer ac.mu.Unlock()
    
    	if ac.watcher.running {
    		return // A watcher is running and will keep the cache up-to-date.
    	}
    	if ac.throttle == nil {
    		ac.throttle = time.NewTimer(0)
    	} else {
    		select {
    		case <-ac.throttle.C:
    		default:
    			return // The cache was reloaded recently.
    		}
    	}
    	ac.watcher.start()
    	ac.reload()
    	ac.throttle.Reset(minReloadInterval)
    }

    首先使用goroutine启动一个watcher来监测keystore目录,防止其变化。接下来调用reload方法加载账户信息,reload方法的实现如下:

    func (ac *accountCache) reload() {
    	accounts, err := ac.scan()
    	if err != nil {
    		log.Debug("Failed to reload keystore contents", "err", err)
    	}
    	ac.all = accounts
    	sort.Sort(ac.all)
    	for k := range ac.byAddr {
    		delete(ac.byAddr, k)
    	}
    	for _, a := range accounts {
    		ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a)
    	}
    	select {
    	case ac.notify <- struct{}{}:
    	default:
    	}
    	log.Debug("Reloaded keystore contents", "accounts", len(ac.all))
    }

    reload方法首先加载Account信息,然后通过channel的方式通知加载结束。Scan方法就是最终从文件中加载账户信息的地方,其实现如下:

    func (ac *accountCache) scan() ([]accounts.Account, error) {
    	files, err := ioutil.ReadDir(ac.keydir)
    	if err != nil {
    		return nil, err
    	}
    
    	var (
    		buf     = new(bufio.Reader)
    		addrs   []accounts.Account
    		keyJSON struct {
    			Address string `json:"address"`
    		}
    	)
    	for _, fi := range files {
    		path := filepath.Join(ac.keydir, fi.Name())
    		if skipKeyFile(fi) {
    			log.Trace("Ignoring file on account scan", "path", path)
    			continue
    		}
    		logger := log.New("path", path)
    
    		fd, err := os.Open(path)
    		if err != nil {
    			logger.Trace("Failed to open keystore file", "err", err)
    			continue
    		}
    		buf.Reset(fd)
    		// Parse the address.
    		keyJSON.Address = ""
    		err = json.NewDecoder(buf).Decode(&keyJSON)
    		addr := common.HexToAddress(keyJSON.Address)
    		switch {
    		case err != nil:
    			logger.Debug("Failed to decode keystore key", "err", err)
    		case (addr == common.Address{}):
    			logger.Debug("Failed to decode keystore key", "err", "missing or zero address")
    		default:
    			addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
    		}
    		fd.Close()
    	}
    	return addrs, err
    }

    该方法首先获取该目录下的文件,然后读取文件内容,JSON解析其address字段。下面是我的datadir/keystore中文件的内容,格式化后如下:

    {
    	"address":"a8f8687d0da839cef651cc0d2dc41bb2dc796293",
    	"crypto":{
    		"cipher":"aes-128-ctr",
    		"ciphertext":"142da6b140f5f4ddd48205997cfc6257312dde63e227579bdc		a427a24661b9f8",
    		"cipherparams":{
    		"iv":"8d45e1d8b9fbd72fa40799c3a2ca1d22"
    	},
    	"kdf":"scrypt",
    	"kdfparams":{
    		"dklen":32,
    		"n":262144,
    		"p":1,
    		"r":8,
    		"salt":"68d9776e818a5c1a92982752df1b3c66fc54a804d508a926519d33e046158959"
    		},
    	"mac":"898b1f94fda7731215e521ea7ca7eb7d9afd9b8255c6dccd244c78af88123e7e"
    	},
    	"id":"dfefe914-e328-4f7f-89a7-a85689b8e855",
    	"version":3
    }

    从json可以看出我这里只有一个账户的信息,因为以太坊对于每个用户都创建一个单独的文件来保存其信息。对于账户信息的加载,AccountManager的创建我们分析完了,同时整个Node也创建完成,此时我们来看以太坊是如何启动一个节点的。此时回到main函数,startNode函数的代码如下:

    func startNode(ctx *cli.Context, stack *node.Node) {
    	// Start up the node itself
    	utils.StartNode(stack)
    
    	// Unlock any account specifically requested
    	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)
    		}
    	}
    	// Register wallet event handlers to open and auto-derive wallets
    	events := make(chan accounts.WalletEvent, 16)
    	stack.AccountManager().Subscribe(events)
    
    	go func() {
    		// Create an chain state reader for self-derivation
    		rpcClient, err := stack.Attach()
    		if err != nil {
    			utils.Fatalf("Failed to attach to self: %v", err)
    		}
    		stateReader := ethclient.NewClient(rpcClient)
    
    		// Open and self derive any wallets already attached
    		for _, wallet := range stack.AccountManager().Wallets() {
    			if err := wallet.Open(""); err != nil {
    				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
    			} else {
    				wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
    			}
    		}
    		// Listen for wallet event till termination
    		for event := range events {
    			if event.Arrive {
    				if err := event.Wallet.Open(""); err != nil {
    					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
    				} else {
    					log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", event.Wallet.Status())
    					event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
    				}
    			} else {
    				log.Info("Old wallet dropped", "url", event.Wallet.URL())
    				event.Wallet.Close()
    			}
    		}
    	}()
    	// Start auxiliary services if enabled
    	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
    		// Mining only makes sense if a full Ethereum node is running
    		var ethereum *eth.Ethereum
    		if err := stack.Service(&ethereum); err != nil {
    			utils.Fatalf("ethereum service not running: %v", err)
    		}
    		// Use a reduced number of threads if requested
    		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)
    			}
    		}
    		// Set the gas price to the limits from the CLI and start mining
    		ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
    		if err := ethereum.StartMining(true); err != nil {
    			utils.Fatalf("Failed to start mining: %v", err)
    		}
    	}
    }

    该函数内部首先将节点启动起来,然后建立跟RPC Server建立一个连接,用于RPC通信。我们来看看节点如何启动起来的。/cmd/utils/cmd.go文件如下:

    func StartNode(stack *node.Node) {
    	if err := stack.Start(); err != nil {
    		Fatalf("Error starting protocol stack: %v", err)
    	}
    	go func() {
    		sigc := make(chan os.Signal, 1)
    		signal.Notify(sigc, os.Interrupt)
    		defer signal.Stop(sigc)
    		<-sigc
    		log.Info("Got interrupt, shutting down...")
    		go stack.Stop()
    		for i := 10; i > 0; i-- {
    			<-sigc
    			if i > 1 {
    				log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
    			}
    		}
    		debug.Exit() // ensure trace and CPU profile data is flushed.
    		debug.LoudPanic("boom")
    	}()
    }

    该函数调用了Node.go中的start方法,其实现如下:

    func (n *Node) Start() error {
    	n.lock.Lock()
    	defer n.lock.Unlock()
    
    	// Short circuit if the node's already running
    	if n.server != nil {
    		return ErrNodeRunning
    	}
    	if err := n.openDataDir(); err != nil {
    		return err
    	}
    
    	// Initialize the p2p server. This creates the node key and
    	// discovery databases.
    	n.serverConfig = n.config.P2P
    	n.serverConfig.PrivateKey = n.config.NodeKey()
    	n.serverConfig.Name = n.config.NodeName()
    	if n.serverConfig.StaticNodes == nil {
    		n.serverConfig.StaticNodes = n.config.StaticNodes()
    	}
    	if n.serverConfig.TrustedNodes == nil {
    		n.serverConfig.TrustedNodes = n.config.TrustedNodes()
    	}
    	if n.serverConfig.NodeDatabase == "" {
    		n.serverConfig.NodeDatabase = n.config.NodeDB()
    	}
    	running := &p2p.Server{Config: n.serverConfig}
    	log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)
    
    	// Otherwise copy and specialize the P2P configuration
    	services := make(map[reflect.Type]Service)
    	for _, constructor := range n.serviceFuncs {
    		// Create a new context for the particular service
    		ctx := &ServiceContext{
    			config:         n.config,
    			services:       make(map[reflect.Type]Service),
    			EventMux:       n.eventmux,
    			AccountManager: n.accman,
    		}
    		for kind, s := range services { // copy needed for threaded access
    			ctx.services[kind] = s
    		}
    		// Construct and save the service
    		service, err := constructor(ctx)
    		if err != nil {
    			return err
    		}
    		kind := reflect.TypeOf(service)
    		if _, exists := services[kind]; exists {
    			return &DuplicateServiceError{Kind: kind}
    		}
    		services[kind] = service
    	}
    	// Gather the protocols and start the freshly assembled P2P server
    	for _, service := range services {
    		running.Protocols = append(running.Protocols, service.Protocols()...)
    	}
    	if err := running.Start(); err != nil {
    		if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] {
    			return ErrDatadirUsed
    		}
    		return err
    	}
    	// Start each of the services
    	started := []reflect.Type{}
    	for kind, service := range services {
    		// Start the next service, stopping all previous upon failure
    		if err := service.Start(running); err != nil {
    			for _, kind := range started {
    				services[kind].Stop()
    			}
    			running.Stop()
    
    			return err
    		}
    		// Mark the service started for potential cleanup
    		started = append(started, kind)
    	}
    	// Lastly start the configured RPC interfaces
    	if err := n.startRPC(services); err != nil {
    		for _, service := range services {
    			service.Stop()
    		}
    		running.Stop()
    		return err
    	}
    	// Finish initializing the startup
    	n.services = services
    	n.server = running
    	n.stop = make(chan struct{})
    
    	return nil
    }

    该方法首先打开datadir目录,接着初始化serverConfig的相关配置,接着创建一个p2p.server的一个变量,然后启动该节点,在接着把RPC的Service启动起来,我们先看看节点的启动。/p2p/server.go中Start方法如下:

    func (srv *Server) Start() (err error) {
    	srv.lock.Lock()
    	defer srv.lock.Unlock()
    	if srv.running {
    		return errors.New("server already running")
    	}
    	srv.running = true
    	log.Info("Starting P2P networking")
    
    	// static fields
    	if srv.PrivateKey == nil {
    		return fmt.Errorf("Server.PrivateKey must be set to a non-nil key")
    	}
    	if srv.newTransport == nil {
    		srv.newTransport = newRLPX
    	}
    	if srv.Dialer == nil {
    		srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout}
    	}
    	srv.quit = make(chan struct{})
    	srv.addpeer = make(chan *conn)
    	srv.delpeer = make(chan peerDrop)
    	srv.posthandshake = make(chan *conn)
    	srv.addstatic = make(chan *discover.Node)
    	srv.removestatic = make(chan *discover.Node)
    	srv.peerOp = make(chan peerOpFunc)
    	srv.peerOpDone = make(chan struct{})
    
    	// node table
    	if !srv.NoDiscovery {
    		ntab, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT, srv.NodeDatabase, srv.NetRestrict)
    		if err != nil {
    			return err
    		}
    		if err := ntab.SetFallbackNodes(srv.BootstrapNodes); err != nil {
    			return err
    		}
    		srv.ntab = ntab
    	}
    
    	if srv.DiscoveryV5 {
    		ntab, err := discv5.ListenUDP(srv.PrivateKey, srv.DiscoveryV5Addr, srv.NAT, "", srv.NetRestrict) //srv.NodeDatabase)
    		if err != nil {
    			return err
    		}
    		if err := ntab.SetFallbackNodes(srv.BootstrapNodesV5); err != nil {
    			return err
    		}
    		srv.DiscV5 = ntab
    	}
    
    	dynPeers := (srv.MaxPeers + 1) / 2
    	if srv.NoDiscovery {
    		dynPeers = 0
    	}
    	dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict)
    
    	// handshake
    	srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)}
    	for _, p := range srv.Protocols {
    		srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap())
    	}
    	// listen/dial
    	if srv.ListenAddr != "" {
    		if err := srv.startListening(); err != nil {
    			return err
    		}
    	}
    	if srv.NoDial && srv.ListenAddr == "" {
    		log.Warn("P2P server will be useless, neither dialing nor listening")
    	}
    
    	srv.loopWG.Add(1)
    	go srv.run(dialer)
    	srv.running = true
    	return nil
    }

    该方法首先建立一个UDP连接,并且调用server.run启动起来,跟其它可用的节点建立连接等。关于如何建立连接,后续在讲P2P的时候,我们再来仔细的分析。到此,整个节点就启动起来了。接下来,我们来看前面4个步骤中的第二个–创建console实例。console.go中创建一个console的New方法实现如下:

    func New(config Config) (*Console, error) {
    	// Handle unset config values gracefully
    	if config.Prompter == nil {
    		config.Prompter = Stdin
    	}
    	if config.Prompt == "" {
    		config.Prompt = DefaultPrompt
    	}
    	if config.Printer == nil {
    		config.Printer = colorable.NewColorableStdout()
    	}
    	// Initialize the console and return
    	console := &Console{
    		client:   config.Client,
    		jsre:     jsre.New(config.DocRoot, config.Printer),
    		prompt:   config.Prompt,
    		prompter: config.Prompter,
    		printer:  config.Printer,
    		histPath: filepath.Join(config.DataDir, HistoryFile),
    	}
    	if err := console.init(config.Preload); err != nil {
    		return nil, err
    	}
    	return console, nil
    }

    该函数首先对Config做一些默认设置,接着调用其jsre.New函数,在借这个调用init方法初始化相关RPC API。jsre.New方法的实现如下:

    func New(assetPath string, output io.Writer) *JSRE {
    	re := &JSRE{
    		assetPath:     assetPath,
    		output:        output,
    		closed:        make(chan struct{}),
    		evalQueue:     make(chan *evalReq),
    		stopEventLoop: make(chan bool),
    	}
    	go re.runEventLoop()
    	re.Set("loadScript", re.loadScript)
    	re.Set("inspect", re.prettyPrintJS)
    	return re
    }

    该函数创建一个JSRE的变量然后使用goroutine的方式开启一个事件监听的循环,该事件为控制台输入后,经过一些处理通过channel的方式发送过来。具体接收到的命令后续处理,请读者自己跟踪其逻辑实现。总结下创建console的目的主要是监听控制台输入的命名。接下来我们来看看第三个步骤「显示Welcome信息 」,该信息在通过geth进入控制台的时候,会显示一些简单的信息,我们先来看看该函数的实现:

    func (c *Console) Welcome() {
    	// Print some generic Geth metadata
    	fmt.Fprintf(c.printer, "Welcome to the Geth JavaScript console!\n\n")
    	c.jsre.Run(`
    		console.log("instance: " + web3.version.node);
    		console.log("coinbase: " + eth.coinbase);
    		console.log("at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")");
    		console.log(" datadir: " + admin.datadir);
    	`)
    	// List all the supported modules for the user to call
    	if apis, err := c.client.SupportedModules(); err == nil {
    		modules := make([]string, 0, len(apis))
    		for api, version := range apis {
    			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
    		}
    		sort.Strings(modules)
    		fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " "))
    	}
    	fmt.Fprintln(c.printer)
    }

    该方法首先打印「Welcome to the Geth JavaScript console!」信息,这个可以直接在控制台查看到,接着使用上面创建console的jsre执行四个控制台输出写版本、区块等信息。你可以直接复制出来在命令行执行,也能看到相同的结果。下面的截图为该函数的输出信息:AB6060B9-23B1-4E0A-A2B3-A7F494516F5C.png

    接下来,我们来看最后的一个步骤「创建一个无限循环用于在控制台交互」,该函数的实现如下:

    func (c *Console) Interactive() {
    	var (
    		prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
    		indents   = 0                 // Current number of input indents (used for multi-line inputs)
    		input     = ""                // Current user input
    		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
    	)
    	// Start a goroutine to listen for promt requests and send back inputs
    	go func() {
    		for {
    			// Read the next user input
    			line, err := c.prompter.PromptInput(<-scheduler)
    			if err != nil {
    				// In case of an error, either clear the prompt or fail
    				if err == liner.ErrPromptAborted { // ctrl-C
    					prompt, indents, input = c.prompt, 0, ""
    					scheduler <- ""
    					continue
    				}
    				close(scheduler)
    				return
    			}
    			// User input retrieved, send for interpretation and loop
    			scheduler <- line
    		}
    	}()
    	// Monitor Ctrl-C too in case the input is empty and we need to bail
    	abort := make(chan os.Signal, 1)
    	signal.Notify(abort, os.Interrupt)
    
    	// Start sending prompts to the user and reading back inputs
    	for {
    		// Send the next prompt, triggering an input read and process the result
    		scheduler <- prompt
    		select {
    		case <-abort:
    			// User forcefully quite the console
    			fmt.Fprintln(c.printer, "caught interrupt, exiting")
    			return
    
    		case line, ok := <-scheduler:
    			// User input was returned by the prompter, handle special cases
    			if !ok || (indents <= 0 && exit.MatchString(line)) {
    				return
    			}
    			if onlyWhitespace.MatchString(line) {
    				continue
    			}
    			// Append the line to the input and check for multi-line interpretation
    			input += line + "\n"
    
    			indents = countIndents(input)
    			if indents <= 0 {
    				prompt = c.prompt
    			} else {
    				prompt = strings.Repeat(".", indents*3) + " "
    			}
    			// If all the needed lines are present, save the command and run
    			if indents <= 0 {
    				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
    					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
    						c.history = append(c.history, command)
    						if c.prompter != nil {
    							c.prompter.AppendHistory(command)
    						}
    					}
    				}
    				c.Evaluate(input)
    				input = ""
    			}
    		}
    	}
    }

    该方法首先创建一个scheduler通道,用于进程间的通信。接着使用goroutine的方式创建一个线程来监听用户在控制台的输入。接着一个无线for循环来处理从scheduler传递过来用户的输入信息。首先通过从scheduler通道读取数据line,然后做一些数据合法性验证。接着调用console的Evaluate方法将用户输入的命令交给前面的jsre处理(最终调用Do方法向evalQueue通道发送数据),最终前面初始化console时初始化的jsre用于接收用户输入命令的线程接收到数据,然后调用VM来处理,并将结果返回在控制台显示。

    至此,整个以太坊就启动起来了,完成了相关AccountManager的初始化、P2P Node的启动、RPC Server启动以及控制台交互的事件监听。后续我们将分析P2P网络的启动、转账的具体实现等等。对于分析的流程,可能有些地方有错误,麻烦大家指出,感谢~

    以太坊源码分析-开篇13年时第一次知道比特币的存在,那时仅仅是跟风炒币而已,没有具体去了解相关的技术细节。前不久无意中有人提到「比特币在没有大维护的情况下完美运行8年了」,瞬间惊呆了,于是乎开始了解区块链的相关知识。在区块链中,我选择以太坊作为切入点,分析以太坊的相关底层技术细节实现。由于初学不久,分析不对的地方烦请指出。

    以太坊开发环境搭建

    1. 安装Go环境,请自行Google
    2. 从github上clone以太坊的Go语言版本go-ethereum
    3. 命令行直接进入go-ethereum目录,执行「make all」命令,等待编译完成后,在build目录中将生成一个bin目录,该目录中就是我们编译好的相关命令。如下: 58005960-23EA-4C5E-B207-AC2DB664C471.png下面我们来简单的介绍这几个命令的使用场景
    • abigen-- 一个源代码生成器,它将Ethereum智能合约定义(代码) 转换为易于使用的、编译时类型安全的Go package。 如果合约字节码也available的话,它可以在普通的Ethereum智能合约ABI上扩展功能。 同时也能编译Solidity源文件,使开发更加精简。
    • bootnod–此Ethereum客户端实现的剥离版本只参与 网络节点发现 协议,但不运行任何更高级别的应用协议。 它可以用作轻量级引导节点,以帮助在私有网络中查找peers。
    • evm-- 能够在可配置环境和执行模式下运行字节码片段的Developer utility版本的的EVM(Ethereum Virtual Machine)。 其目的是允许对EVM操作码进行封装,细粒度的调试。
    • faucet–暂时不知道其使用场景,其help没有相关的解释,后续看下源码再来补充。
    • geth–主要Ethereum CLI客户端。它是Ethereum网络(以太坊主网,测试网络或私有网)的入口点,使用此命令可以使节点作为full node(默认),或者archive node(保留所有历史状态)或light node(检索数据实时)运行。 其他进程可以通过暴露在HTTP,WebSocket和/或IPC传输之上的JSON RPC端点作为通向Ethereum网络的网关使用。
    • puppeth–暂时不知道其含义,后续补充。
    • rlpdump–开发者通用工具,用来把二进制RLP (Recursive Length Prefix) (Ethereum 协议中用于网络及一致性的数据编码) 转换成用户友好的分层表示。
    • swarm–swarm守护进程和工具,这是swarm网络的进入点。

    接下来我们从以太坊最重要的geth命令入手,来分析以太坊的启动流程。首先,在命令行输入一下命令打开控制台:

    build/bin/geth --datadir=./dev/data0 --networkid 1 console

    接下来我们来看启动入口main函数,它位于/cmd/geth/main.go文件中,main函数的初始化函数代码如下:

    func init() {
    	// Initialize the CLI app and start Geth
    	app.Action = geth
    	app.HideVersion = true // we have a command to print the version
    	app.Copyright = "Copyright 2013-2017 The go-ethereum Authors"
    	app.Commands = []cli.Command{
    		// See chaincmd.go:
    		initCommand,
    		importCommand,
    		exportCommand,
    		removedbCommand,
    		dumpCommand,
    		// See monitorcmd.go:
    		monitorCommand,
    		// See accountcmd.go:
    		accountCommand,
    		walletCommand,
    		// See consolecmd.go:
    		consoleCommand,
    		attachCommand,
    		javascriptCommand,
    		// See misccmd.go:
    		makedagCommand,
    		versionCommand,
    		bugCommand,
    		licenseCommand,
    		// See config.go
    		dumpConfigCommand,
    	}
    	app.Flags = append(app.Flags, nodeFlags...)
    	app.Flags = append(app.Flags, rpcFlags...)
    	app.Flags = append(app.Flags, consoleFlags...)
    	app.Flags = append(app.Flags, debug.Flags...)
    	app.Flags = append(app.Flags, whisperFlags...)
    	app.Before = func(ctx *cli.Context) error {
    		runtime.GOMAXPROCS(runtime.NumCPU())
    		if err := debug.Setup(ctx); err != nil {
    			return err
    		}
    		// Start system runtime metrics collection
    		go metrics.CollectProcessMetrics(3 * time.Second)
    		utils.SetupNetwork(ctx)
    		return nil
    	}
    	app.After = func(ctx *cli.Context) error {
    		debug.Exit()
    		console.Stdin.Close() // Resets terminal mode.
    		return nil
    	}
    }

    init函数主要是做了一些初始化的工作,其中比较重要的有三个地方,app.Action=geth,app.Commands中consoleCommand,以及App.Before指向的匿名函数,后续使用到的时候我们再来分析。我们再来看看main函数:

    func main() {
    	if err := app.Run(os.Args); err != nil {
    		fmt.Fprintln(os.Stderr, err)
    		os.Exit(1)
    	}
    }

    main函数的实现很简单,仅仅调用了app.Run函数,如果调用异常,则退出。接下里我们看看app.Run函数的逻辑处理

    func (a *App) Run(arguments []string) (err error) {
    	a.Setup()
    
    	// handle the completion flag separately from the flagset since
    	// completion could be attempted after a flag, but before its value was put
    	// on the command line. this causes the flagset to interpret the completion
    	// flag name as the value of the flag before it which is undesirable
    	// note that we can only do this because the shell autocomplete function
    	// always appends the completion flag at the end of the command
    	shellComplete, arguments := checkShellCompleteFlag(a, arguments)
    
    	// parse flags
    	set, err := flagSet(a.Name, a.Flags)
    	if err != nil {
    		return err
    	}
    
    	set.SetOutput(ioutil.Discard)
    	err = set.Parse(arguments[1:])
    	nerr := normalizeFlags(a.Flags, set)
    	context := NewContext(a, set, nil)
    	if nerr != nil {
    		fmt.Fprintln(a.Writer, nerr)
    		ShowAppHelp(context)
    		return nerr
    	}
    	context.shellComplete = shellComplete
    
    	if checkCompletions(context) {
    		return nil
    	}
    
    	if err != nil {
    		if a.OnUsageError != nil {
    			err := a.OnUsageError(context, err, false)
    			HandleExitCoder(err)
    			return err
    		}
    		fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
    		ShowAppHelp(context)
    		return err
    	}
    
    	if !a.HideHelp && checkHelp(context) {
    		ShowAppHelp(context)
    		return nil
    	}
    
    	if !a.HideVersion && checkVersion(context) {
    		ShowVersion(context)
    		return nil
    	}
    
    	if a.After != nil {
    		defer func() {
    			if afterErr := a.After(context); afterErr != nil {
    				if err != nil {
    					err = NewMultiError(err, afterErr)
    				} else {
    					err = afterErr
    				}
    			}
    		}()
    	}
    
    	if a.Before != nil {
    		beforeErr := a.Before(context)
    		if beforeErr != nil {
    			fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
    			ShowAppHelp(context)
    			HandleExitCoder(beforeErr)
    			err = beforeErr
    			return err
    		}
    	}
    
    	args := context.Args()
    	if args.Present() {
    		name := args.First()
    		c := a.Command(name)
    		if c != nil {
    			return c.Run(context)
    		}
    	}
    
    	if a.Action == nil {
    		a.Action = helpCommand.Action
    	}
    
    	// Run default Action
    	err = HandleAction(a.Action, context)
    
    	HandleExitCoder(err)
    	return err
    }

    a.Setup仅仅是做了些简单的处理,比如相关的Auther、Email、重新创建Command切片等等.接下来我们看看下面的这个if判断

    if args.Present() {
    		name := args.First()
    		c := a.Command(name)
    		if c != nil {
    			return c.Run(context)
    		}
    	}

    由于我们前面在控制台输入的命令(build/bin/geth --datadir=./dev/data0 --networkid 1 console)长度不为0,因此执行

    c.Run(context)

    操作,此时的命令其实就是我们的console命令。接下来我们看看Run方法,Run方法的代码如下:

    func (c Command) Run(ctx *Context) (err error) {
    	if len(c.Subcommands) > 0 {
    		return c.startApp(ctx)
    	}
    
    	if !c.HideHelp && (HelpFlag != BoolFlag{}) {
    		// append help to flags
    		c.Flags = append(
    			c.Flags,
    			HelpFlag,
    		)
    	}
    
    	set, err := flagSet(c.Name, c.Flags)
    	if err != nil {
    		return err
    	}
    	set.SetOutput(ioutil.Discard)
    
    	if c.SkipFlagParsing {
    		err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
    	} else if !c.SkipArgReorder {
    		firstFlagIndex := -1
    		terminatorIndex := -1
    		for index, arg := range ctx.Args() {
    			if arg == "--" {
    				terminatorIndex = index
    				break
    			} else if arg == "-" {
    				// Do nothing. A dash alone is not really a flag.
    				continue
    			} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
    				firstFlagIndex = index
    			}
    		}
    
    		if firstFlagIndex > -1 {
    			args := ctx.Args()
    			regularArgs := make([]string, len(args[1:firstFlagIndex]))
    			copy(regularArgs, args[1:firstFlagIndex])
    
    			var flagArgs []string
    			if terminatorIndex > -1 {
    				flagArgs = args[firstFlagIndex:terminatorIndex]
    				regularArgs = append(regularArgs, args[terminatorIndex:]...)
    			} else {
    				flagArgs = args[firstFlagIndex:]
    			}
    
    			err = set.Parse(append(flagArgs, regularArgs...))
    		} else {
    			err = set.Parse(ctx.Args().Tail())
    		}
    	} else {
    		err = set.Parse(ctx.Args().Tail())
    	}
    
    	nerr := normalizeFlags(c.Flags, set)
    	if nerr != nil {
    		fmt.Fprintln(ctx.App.Writer, nerr)
    		fmt.Fprintln(ctx.App.Writer)
    		ShowCommandHelp(ctx, c.Name)
    		return nerr
    	}
    
    	context := NewContext(ctx.App, set, ctx)
    	if checkCommandCompletions(context, c.Name) {
    		return nil
    	}
    
    	if err != nil {
    		if c.OnUsageError != nil {
    			err := c.OnUsageError(ctx, err, false)
    			HandleExitCoder(err)
    			return err
    		}
    		fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error())
    		fmt.Fprintln(ctx.App.Writer)
    		ShowCommandHelp(ctx, c.Name)
    		return err
    	}
    
    	if checkCommandHelp(context, c.Name) {
    		return nil
    	}
    
    	if c.After != nil {
    		defer func() {
    			afterErr := c.After(context)
    			if afterErr != nil {
    				HandleExitCoder(err)
    				if err != nil {
    					err = NewMultiError(err, afterErr)
    				} else {
    					err = afterErr
    				}
    			}
    		}()
    	}
    
    	if c.Before != nil {
    		err = c.Before(context)
    		if err != nil {
    			fmt.Fprintln(ctx.App.Writer, err)
    			fmt.Fprintln(ctx.App.Writer)
    			ShowCommandHelp(ctx, c.Name)
    			HandleExitCoder(err)
    			return err
    		}
    	}
    
    	if c.Action == nil {
    		c.Action = helpSubcommand.Action
    	}
    
    	context.Command = c
    	err = HandleAction(c.Action, context)
    
    	if err != nil {
    		HandleExitCoder(err)
    	}
    	return err
    }

    该主要是设置flag、解析输入的命令行参数、创建全局的context、将当前命令保存到全局context中,接下来调用HandleAction来处理命令,HandleAction的函数实现如下:

    func HandleAction(action interface{}, context *Context) (err error) {
    	if a, ok := action.(ActionFunc); ok {
    		return a(context)
    	} else if a, ok := action.(func(*Context) error); ok {
    		return a(context)
    	} else if a, ok := action.(func(*Context)); ok { // deprecated function signature
    		a(context)
    		return nil
    	} else {
    		return errInvalidActionType
    	}
    }

    action的类型是「func(*Context) error」,此时将执行a(context)方法,那么此时调用那个Action呢,答案就是我们前面提到的App.init()初始化命令时的consoleCommand,接下来我们来看看cmd/geth/consolecmd中的consoleCommand:

    consoleCommand = cli.Command{
    		Action:   utils.MigrateFlags(localConsole),
    		Name:     "console",
    		Usage:    "Start an interactive JavaScript environment",
    		Flags:    append(append(append(nodeFlags, rpcFlags...), consoleFlags...), whisperFlags...),
    		Category: "CONSOLE COMMANDS",
    		Description: `
    The Geth console is an interactive shell for the JavaScript runtime environment
    which exposes a node admin interface as well as the Ðapp JavaScript API.
    See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.`,
    	}

    其Action中的localConsole函数如下:

    func localConsole(ctx *cli.Context) error {
    	// Create and start the node based on the CLI flags
    	node := makeFullNode(ctx)
    	startNode(ctx, node)
    	defer node.Stop()
    
    	// Attach to the newly started node and start the JavaScript console
    	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 only a short execution was requested, evaluate and return
    	if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
    		console.Evaluate(script)
    		return nil
    	}
    	// Otherwise print the welcome screen and enter interactive mode
    	console.Welcome()
    	console.Interactive()
    
    	return nil
    }
    

    该函数非常重要,主要完成以下几件事情

    1. 首先会创建一个节点、同时启动该节点
    2. 创建一个console的实例
    3. 显示Welcome信息
    4. 创建一个无限循环用于在控制台交互

    首先我们来看该Node是如何创建的。makeFullNode函数的实现如下:

    func makeFullNode(ctx *cli.Context) *node.Node {
    	stack, cfg := makeConfigNode(ctx)
    
    	utils.RegisterEthService(stack, &cfg.Eth)
    
    	// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
    	shhEnabled := enableWhisper(ctx)
    	shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)
    	if shhEnabled || shhAutoEnabled {
    		if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
    			cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
    		}
    		if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
    			cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
    		}
    		utils.RegisterShhService(stack, &cfg.Shh)
    	}
    
    	// Add the Ethereum Stats daemon if requested.
    	if cfg.Ethstats.URL != "" {
    		utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
    	}
    
    	// Add the release oracle service so it boots along with node.
    	if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
    		config := release.Config{
    			Oracle: relOracle,
    			Major:  uint32(params.VersionMajor),
    			Minor:  uint32(params.VersionMinor),
    			Patch:  uint32(params.VersionPatch),
    		}
    		commit, _ := hex.DecodeString(gitCommit)
    		copy(config.Commit[:], commit)
    		return release.NewReleaseService(ctx, config)
    	}); err != nil {
    		utils.Fatalf("Failed to register the Geth release oracle service: %v", err)
    	}
    	return stack
    }

    该函数首先创建一个Node,然后注册一个Ethereum Service,我们继续分析Node是如何创建的,makeConfigNode的函数实现逻辑如下:

    func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
    	// Load defaults.
    	cfg := gethConfig{
    		Eth:  eth.DefaultConfig,
    		Shh:  whisper.DefaultConfig,
    		Node: defaultNodeConfig(),
    	}
    
    	// Load config file.
    	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
    		if err := loadConfig(file, &cfg); err != nil {
    			utils.Fatalf("%v", err)
    		}
    	}
    
    	// Apply flags.
    	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)
    
    	return stack, cfg
    }

    首先初始化gethConfig的变量,接下来调用node.New方法来创建一个Node,我们来看看New函数的实现:

    func New(conf *Config) (*Node, error) {
    	// Copy config and resolve the datadir so future changes to the current
    	// working directory don't affect the node.
    	confCopy := *conf
    	conf = &confCopy
    	if conf.DataDir != "" {
    		absdatadir, err := filepath.Abs(conf.DataDir)
    		if err != nil {
    			return nil, err
    		}
    		conf.DataDir = absdatadir
    	}
    	// Ensure that the instance name doesn't cause weird conflicts with
    	// other files in the data directory.
    	if strings.ContainsAny(conf.Name, `/\`) {
    		return nil, errors.New(`Config.Name must not contain '/' or '\'`)
    	}
    	if conf.Name == datadirDefaultKeyStore {
    		return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
    	}
    	if strings.HasSuffix(conf.Name, ".ipc") {
    		return nil, errors.New(`Config.Name cannot end in ".ipc"`)
    	}
    	// Ensure that the AccountManager method works before the node has started.
    	// We rely on this in cmd/geth.
    	am, ephemeralKeystore, err := makeAccountManager(conf)
    	if err != nil {
    		return nil, err
    	}
    	// Note: any interaction with Config that would create/touch files
    	// in the data directory or instance directory is delayed until Start.
    	return &Node{
    		accman:            am,
    		ephemeralKeystore: ephemeralKeystore,
    		config:            conf,
    		serviceFuncs:      []ServiceConstructor{},
    		ipcEndpoint:       conf.IPCEndpoint(),
    		httpEndpoint:      conf.HTTPEndpoint(),
    		wsEndpoint:        conf.WSEndpoint(),
    		eventmux:          new(event.TypeMux),
    	}, nil
    }

    New函数首先将保存数据的目录转换成绝对路径,然后对配置进行相关的验证,确认是否合法,接下来创建一个AccountManager实例,然后创建一个Node节点,总体来说其内部的逻辑比较的简单。由于AccountManager负责Account的相关管理操作且后续经常使用,接下来我们看看是AccountManager的具体逻辑实现。AccountManager位于/node/config.go文件中,其实现如下:

    func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
    	scryptN := keystore.StandardScryptN
    	scryptP := keystore.StandardScryptP
    	if conf.UseLightweightKDF {
    		scryptN = keystore.LightScryptN
    		scryptP = keystore.LightScryptP
    	}
    
    	var (
    		keydir    string
    		ephemeral string
    		err       error
    	)
    	switch {
    	case filepath.IsAbs(conf.KeyStoreDir):
    		keydir = conf.KeyStoreDir
    	case conf.DataDir != "":
    		if conf.KeyStoreDir == "" {
    			keydir = filepath.Join(conf.DataDir, datadirDefaultKeyStore)
    		} else {
    			keydir, err = filepath.Abs(conf.KeyStoreDir)
    		}
    	case conf.KeyStoreDir != "":
    		keydir, err = filepath.Abs(conf.KeyStoreDir)
    	default:
    		// There is no datadir.
    		keydir, err = ioutil.TempDir("", "go-ethereum-keystore")
    		ephemeral = keydir
    	}
    	if err != nil {
    		return nil, "", err
    	}
    	if err := os.MkdirAll(keydir, 0700); err != nil {
    		return nil, "", err
    	}
    	// Assemble the account manager and supported backends
    	backends := []accounts.Backend{
    		keystore.NewKeyStore(keydir, scryptN, scryptP),
    	}
    	if !conf.NoUSB {
    		if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
    			log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
    		} else {
    			backends = append(backends, ledgerhub)
    		}
    	}
    	return accounts.NewManager(backends...), ephemeral, nil
    }

    由于在上面,我们已经将保存数据的目录转换成了绝对路径(由于在命令行,我们已经指定了datadir且存在,如果不存在将创建一个默认的go-ethereum-keystore来保存相关信息),接下来我们来看keystore.NewKeyStore函数。该函数位于/accounts/keystore/keystore.go文件中,其实现如下:

    func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
    	keydir, _ = filepath.Abs(keydir)
    	ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
    	ks.init(keydir)
    	return ks
    }

    首先转换成绝对路径(前面已经转换,应该没有必要再次检测),接下来我们看看

    ks.init(keydir)

    方法的实现,其实现如下:

    
    func (ks *KeyStore) init(keydir string) {
    	// Lock the mutex since the account cache might call back with events
    	ks.mu.Lock()
    	defer ks.mu.Unlock()
    
    	// Initialize the set of unlocked keys and the account cache
    	ks.unlocked = make(map[common.Address]*unlocked)
    	ks.cache, ks.changes = newAccountCache(keydir)
    
    	// TODO: In order for this finalizer to work, there must be no references
    	// to ks. addressCache doesn't keep a reference but unlocked keys do,
    	// so the finalizer will not trigger until all timed unlocks have expired.
    	runtime.SetFinalizer(ks, func(m *KeyStore) {
    		m.cache.close()
    	})
    	// Create the initial list of wallets from the cache
    	accs := ks.cache.accounts()
    	ks.wallets = make([]accounts.Wallet, len(accs))
    	for i := 0; i < len(accs); i++ {
    		ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks}
    	}
    }

    该方法首先从缓存中(如果缓存存在则直接获取,否则从datadir中读取)获取Account,然后保存在keystore的钱包中,我们主要来看看ks.cache.accounts()函数,其实现逻辑如下:

    func (ac *accountCache) accounts() []accounts.Account {
    	ac.maybeReload()
    	ac.mu.Lock()
    	defer ac.mu.Unlock()
    	cpy := make([]accounts.Account, len(ac.all))
    	copy(cpy, ac.all)
    	return cpy
    }

    如果账户数据未被加载到内存中,则首先加载进来,然后copy一份,防止外面对缓存的账户做修改,我们来看看ac.maybeReload是如何加载账户信息的。其实现如下:

    func (ac *accountCache) maybeReload() {
    	ac.mu.Lock()
    	defer ac.mu.Unlock()
    
    	if ac.watcher.running {
    		return // A watcher is running and will keep the cache up-to-date.
    	}
    	if ac.throttle == nil {
    		ac.throttle = time.NewTimer(0)
    	} else {
    		select {
    		case <-ac.throttle.C:
    		default:
    			return // The cache was reloaded recently.
    		}
    	}
    	ac.watcher.start()
    	ac.reload()
    	ac.throttle.Reset(minReloadInterval)
    }

    首先使用goroutine启动一个watcher来监测keystore目录,防止其变化。接下来调用reload方法加载账户信息,reload方法的实现如下:

    func (ac *accountCache) reload() {
    	accounts, err := ac.scan()
    	if err != nil {
    		log.Debug("Failed to reload keystore contents", "err", err)
    	}
    	ac.all = accounts
    	sort.Sort(ac.all)
    	for k := range ac.byAddr {
    		delete(ac.byAddr, k)
    	}
    	for _, a := range accounts {
    		ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a)
    	}
    	select {
    	case ac.notify <- struct{}{}:
    	default:
    	}
    	log.Debug("Reloaded keystore contents", "accounts", len(ac.all))
    }

    reload方法首先加载Account信息,然后通过channel的方式通知加载结束。Scan方法就是最终从文件中加载账户信息的地方,其实现如下:

    func (ac *accountCache) scan() ([]accounts.Account, error) {
    	files, err := ioutil.ReadDir(ac.keydir)
    	if err != nil {
    		return nil, err
    	}
    
    	var (
    		buf     = new(bufio.Reader)
    		addrs   []accounts.Account
    		keyJSON struct {
    			Address string `json:"address"`
    		}
    	)
    	for _, fi := range files {
    		path := filepath.Join(ac.keydir, fi.Name())
    		if skipKeyFile(fi) {
    			log.Trace("Ignoring file on account scan", "path", path)
    			continue
    		}
    		logger := log.New("path", path)
    
    		fd, err := os.Open(path)
    		if err != nil {
    			logger.Trace("Failed to open keystore file", "err", err)
    			continue
    		}
    		buf.Reset(fd)
    		// Parse the address.
    		keyJSON.Address = ""
    		err = json.NewDecoder(buf).Decode(&keyJSON)
    		addr := common.HexToAddress(keyJSON.Address)
    		switch {
    		case err != nil:
    			logger.Debug("Failed to decode keystore key", "err", err)
    		case (addr == common.Address{}):
    			logger.Debug("Failed to decode keystore key", "err", "missing or zero address")
    		default:
    			addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
    		}
    		fd.Close()
    	}
    	return addrs, err
    }

    该方法首先获取该目录下的文件,然后读取文件内容,JSON解析其address字段。下面是我的datadir/keystore中文件的内容,格式化后如下:

    {
    	"address":"a8f8687d0da839cef651cc0d2dc41bb2dc796293",
    	"crypto":{
    		"cipher":"aes-128-ctr",
    		"ciphertext":"142da6b140f5f4ddd48205997cfc6257312dde63e227579bdc		a427a24661b9f8",
    		"cipherparams":{
    		"iv":"8d45e1d8b9fbd72fa40799c3a2ca1d22"
    	},
    	"kdf":"scrypt",
    	"kdfparams":{
    		"dklen":32,
    		"n":262144,
    		"p":1,
    		"r":8,
    		"salt":"68d9776e818a5c1a92982752df1b3c66fc54a804d508a926519d33e046158959"
    		},
    	"mac":"898b1f94fda7731215e521ea7ca7eb7d9afd9b8255c6dccd244c78af88123e7e"
    	},
    	"id":"dfefe914-e328-4f7f-89a7-a85689b8e855",
    	"version":3
    }

    从json可以看出我这里只有一个账户的信息,因为以太坊对于每个用户都创建一个单独的文件来保存其信息。对于账户信息的加载,AccountManager的创建我们分析完了,同时整个Node也创建完成,此时我们来看以太坊是如何启动一个节点的。此时回到main函数,startNode函数的代码如下:

    func startNode(ctx *cli.Context, stack *node.Node) {
    	// Start up the node itself
    	utils.StartNode(stack)
    
    	// Unlock any account specifically requested
    	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)
    		}
    	}
    	// Register wallet event handlers to open and auto-derive wallets
    	events := make(chan accounts.WalletEvent, 16)
    	stack.AccountManager().Subscribe(events)
    
    	go func() {
    		// Create an chain state reader for self-derivation
    		rpcClient, err := stack.Attach()
    		if err != nil {
    			utils.Fatalf("Failed to attach to self: %v", err)
    		}
    		stateReader := ethclient.NewClient(rpcClient)
    
    		// Open and self derive any wallets already attached
    		for _, wallet := range stack.AccountManager().Wallets() {
    			if err := wallet.Open(""); err != nil {
    				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
    			} else {
    				wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
    			}
    		}
    		// Listen for wallet event till termination
    		for event := range events {
    			if event.Arrive {
    				if err := event.Wallet.Open(""); err != nil {
    					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
    				} else {
    					log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", event.Wallet.Status())
    					event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
    				}
    			} else {
    				log.Info("Old wallet dropped", "url", event.Wallet.URL())
    				event.Wallet.Close()
    			}
    		}
    	}()
    	// Start auxiliary services if enabled
    	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
    		// Mining only makes sense if a full Ethereum node is running
    		var ethereum *eth.Ethereum
    		if err := stack.Service(&ethereum); err != nil {
    			utils.Fatalf("ethereum service not running: %v", err)
    		}
    		// Use a reduced number of threads if requested
    		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)
    			}
    		}
    		// Set the gas price to the limits from the CLI and start mining
    		ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
    		if err := ethereum.StartMining(true); err != nil {
    			utils.Fatalf("Failed to start mining: %v", err)
    		}
    	}
    }

    该函数内部首先将节点启动起来,然后建立跟RPC Server建立一个连接,用于RPC通信。我们来看看节点如何启动起来的。/cmd/utils/cmd.go文件如下:

    func StartNode(stack *node.Node) {
    	if err := stack.Start(); err != nil {
    		Fatalf("Error starting protocol stack: %v", err)
    	}
    	go func() {
    		sigc := make(chan os.Signal, 1)
    		signal.Notify(sigc, os.Interrupt)
    		defer signal.Stop(sigc)
    		<-sigc
    		log.Info("Got interrupt, shutting down...")
    		go stack.Stop()
    		for i := 10; i > 0; i-- {
    			<-sigc
    			if i > 1 {
    				log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
    			}
    		}
    		debug.Exit() // ensure trace and CPU profile data is flushed.
    		debug.LoudPanic("boom")
    	}()
    }

    该函数调用了Node.go中的start方法,其实现如下:

    func (n *Node) Start() error {
    	n.lock.Lock()
    	defer n.lock.Unlock()
    
    	// Short circuit if the node's already running
    	if n.server != nil {
    		return ErrNodeRunning
    	}
    	if err := n.openDataDir(); err != nil {
    		return err
    	}
    
    	// Initialize the p2p server. This creates the node key and
    	// discovery databases.
    	n.serverConfig = n.config.P2P
    	n.serverConfig.PrivateKey = n.config.NodeKey()
    	n.serverConfig.Name = n.config.NodeName()
    	if n.serverConfig.StaticNodes == nil {
    		n.serverConfig.StaticNodes = n.config.StaticNodes()
    	}
    	if n.serverConfig.TrustedNodes == nil {
    		n.serverConfig.TrustedNodes = n.config.TrustedNodes()
    	}
    	if n.serverConfig.NodeDatabase == "" {
    		n.serverConfig.NodeDatabase = n.config.NodeDB()
    	}
    	running := &p2p.Server{Config: n.serverConfig}
    	log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)
    
    	// Otherwise copy and specialize the P2P configuration
    	services := make(map[reflect.Type]Service)
    	for _, constructor := range n.serviceFuncs {
    		// Create a new context for the particular service
    		ctx := &ServiceContext{
    			config:         n.config,
    			services:       make(map[reflect.Type]Service),
    			EventMux:       n.eventmux,
    			AccountManager: n.accman,
    		}
    		for kind, s := range services { // copy needed for threaded access
    			ctx.services[kind] = s
    		}
    		// Construct and save the service
    		service, err := constructor(ctx)
    		if err != nil {
    			return err
    		}
    		kind := reflect.TypeOf(service)
    		if _, exists := services[kind]; exists {
    			return &DuplicateServiceError{Kind: kind}
    		}
    		services[kind] = service
    	}
    	// Gather the protocols and start the freshly assembled P2P server
    	for _, service := range services {
    		running.Protocols = append(running.Protocols, service.Protocols()...)
    	}
    	if err := running.Start(); err != nil {
    		if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] {
    			return ErrDatadirUsed
    		}
    		return err
    	}
    	// Start each of the services
    	started := []reflect.Type{}
    	for kind, service := range services {
    		// Start the next service, stopping all previous upon failure
    		if err := service.Start(running); err != nil {
    			for _, kind := range started {
    				services[kind].Stop()
    			}
    			running.Stop()
    
    			return err
    		}
    		// Mark the service started for potential cleanup
    		started = append(started, kind)
    	}
    	// Lastly start the configured RPC interfaces
    	if err := n.startRPC(services); err != nil {
    		for _, service := range services {
    			service.Stop()
    		}
    		running.Stop()
    		return err
    	}
    	// Finish initializing the startup
    	n.services = services
    	n.server = running
    	n.stop = make(chan struct{})
    
    	return nil
    }

    该方法首先打开datadir目录,接着初始化serverConfig的相关配置,接着创建一个p2p.server的一个变量,然后启动该节点,在接着把RPC的Service启动起来,我们先看看节点的启动。/p2p/server.go中Start方法如下:

    func (srv *Server) Start() (err error) {
    	srv.lock.Lock()
    	defer srv.lock.Unlock()
    	if srv.running {
    		return errors.New("server already running")
    	}
    	srv.running = true
    	log.Info("Starting P2P networking")
    
    	// static fields
    	if srv.PrivateKey == nil {
    		return fmt.Errorf("Server.PrivateKey must be set to a non-nil key")
    	}
    	if srv.newTransport == nil {
    		srv.newTransport = newRLPX
    	}
    	if srv.Dialer == nil {
    		srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout}
    	}
    	srv.quit = make(chan struct{})
    	srv.addpeer = make(chan *conn)
    	srv.delpeer = make(chan peerDrop)
    	srv.posthandshake = make(chan *conn)
    	srv.addstatic = make(chan *discover.Node)
    	srv.removestatic = make(chan *discover.Node)
    	srv.peerOp = make(chan peerOpFunc)
    	srv.peerOpDone = make(chan struct{})
    
    	// node table
    	if !srv.NoDiscovery {
    		ntab, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT, srv.NodeDatabase, srv.NetRestrict)
    		if err != nil {
    			return err
    		}
    		if err := ntab.SetFallbackNodes(srv.BootstrapNodes); err != nil {
    			return err
    		}
    		srv.ntab = ntab
    	}
    
    	if srv.DiscoveryV5 {
    		ntab, err := discv5.ListenUDP(srv.PrivateKey, srv.DiscoveryV5Addr, srv.NAT, "", srv.NetRestrict) //srv.NodeDatabase)
    		if err != nil {
    			return err
    		}
    		if err := ntab.SetFallbackNodes(srv.BootstrapNodesV5); err != nil {
    			return err
    		}
    		srv.DiscV5 = ntab
    	}
    
    	dynPeers := (srv.MaxPeers + 1) / 2
    	if srv.NoDiscovery {
    		dynPeers = 0
    	}
    	dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict)
    
    	// handshake
    	srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)}
    	for _, p := range srv.Protocols {
    		srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap())
    	}
    	// listen/dial
    	if srv.ListenAddr != "" {
    		if err := srv.startListening(); err != nil {
    			return err
    		}
    	}
    	if srv.NoDial && srv.ListenAddr == "" {
    		log.Warn("P2P server will be useless, neither dialing nor listening")
    	}
    
    	srv.loopWG.Add(1)
    	go srv.run(dialer)
    	srv.running = true
    	return nil
    }

    该方法首先建立一个UDP连接,并且调用server.run启动起来,跟其它可用的节点建立连接等。关于如何建立连接,后续在讲P2P的时候,我们再来仔细的分析。到此,整个节点就启动起来了。接下来,我们来看前面4个步骤中的第二个–创建console实例。console.go中创建一个console的New方法实现如下:

    func New(config Config) (*Console, error) {
    	// Handle unset config values gracefully
    	if config.Prompter == nil {
    		config.Prompter = Stdin
    	}
    	if config.Prompt == "" {
    		config.Prompt = DefaultPrompt
    	}
    	if config.Printer == nil {
    		config.Printer = colorable.NewColorableStdout()
    	}
    	// Initialize the console and return
    	console := &Console{
    		client:   config.Client,
    		jsre:     jsre.New(config.DocRoot, config.Printer),
    		prompt:   config.Prompt,
    		prompter: config.Prompter,
    		printer:  config.Printer,
    		histPath: filepath.Join(config.DataDir, HistoryFile),
    	}
    	if err := console.init(config.Preload); err != nil {
    		return nil, err
    	}
    	return console, nil
    }

    该函数首先对Config做一些默认设置,接着调用其jsre.New函数,在借这个调用init方法初始化相关RPC API。jsre.New方法的实现如下:

    func New(assetPath string, output io.Writer) *JSRE {
    	re := &JSRE{
    		assetPath:     assetPath,
    		output:        output,
    		closed:        make(chan struct{}),
    		evalQueue:     make(chan *evalReq),
    		stopEventLoop: make(chan bool),
    	}
    	go re.runEventLoop()
    	re.Set("loadScript", re.loadScript)
    	re.Set("inspect", re.prettyPrintJS)
    	return re
    }

    该函数创建一个JSRE的变量然后使用goroutine的方式开启一个事件监听的循环,该事件为控制台输入后,经过一些处理通过channel的方式发送过来。具体接收到的命令后续处理,请读者自己跟踪其逻辑实现。总结下创建console的目的主要是监听控制台输入的命名。接下来我们来看看第三个步骤「显示Welcome信息 」,该信息在通过geth进入控制台的时候,会显示一些简单的信息,我们先来看看该函数的实现:

    func (c *Console) Welcome() {
    	// Print some generic Geth metadata
    	fmt.Fprintf(c.printer, "Welcome to the Geth JavaScript console!\n\n")
    	c.jsre.Run(`
    		console.log("instance: " + web3.version.node);
    		console.log("coinbase: " + eth.coinbase);
    		console.log("at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")");
    		console.log(" datadir: " + admin.datadir);
    	`)
    	// List all the supported modules for the user to call
    	if apis, err := c.client.SupportedModules(); err == nil {
    		modules := make([]string, 0, len(apis))
    		for api, version := range apis {
    			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
    		}
    		sort.Strings(modules)
    		fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " "))
    	}
    	fmt.Fprintln(c.printer)
    }

    该方法首先打印「Welcome to the Geth JavaScript console!」信息,这个可以直接在控制台查看到,接着使用上面创建console的jsre执行四个控制台输出写版本、区块等信息。你可以直接复制出来在命令行执行,也能看到相同的结果。下面的截图为该函数的输出信息:AB6060B9-23B1-4E0A-A2B3-A7F494516F5C.png

    接下来,我们来看最后的一个步骤「创建一个无限循环用于在控制台交互」,该函数的实现如下:

    func (c *Console) Interactive() {
    	var (
    		prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
    		indents   = 0                 // Current number of input indents (used for multi-line inputs)
    		input     = ""                // Current user input
    		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
    	)
    	// Start a goroutine to listen for promt requests and send back inputs
    	go func() {
    		for {
    			// Read the next user input
    			line, err := c.prompter.PromptInput(<-scheduler)
    			if err != nil {
    				// In case of an error, either clear the prompt or fail
    				if err == liner.ErrPromptAborted { // ctrl-C
    					prompt, indents, input = c.prompt, 0, ""
    					scheduler <- ""
    					continue
    				}
    				close(scheduler)
    				return
    			}
    			// User input retrieved, send for interpretation and loop
    			scheduler <- line
    		}
    	}()
    	// Monitor Ctrl-C too in case the input is empty and we need to bail
    	abort := make(chan os.Signal, 1)
    	signal.Notify(abort, os.Interrupt)
    
    	// Start sending prompts to the user and reading back inputs
    	for {
    		// Send the next prompt, triggering an input read and process the result
    		scheduler <- prompt
    		select {
    		case <-abort:
    			// User forcefully quite the console
    			fmt.Fprintln(c.printer, "caught interrupt, exiting")
    			return
    
    		case line, ok := <-scheduler:
    			// User input was returned by the prompter, handle special cases
    			if !ok || (indents <= 0 && exit.MatchString(line)) {
    				return
    			}
    			if onlyWhitespace.MatchString(line) {
    				continue
    			}
    			// Append the line to the input and check for multi-line interpretation
    			input += line + "\n"
    
    			indents = countIndents(input)
    			if indents <= 0 {
    				prompt = c.prompt
    			} else {
    				prompt = strings.Repeat(".", indents*3) + " "
    			}
    			// If all the needed lines are present, save the command and run
    			if indents <= 0 {
    				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
    					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
    						c.history = append(c.history, command)
    						if c.prompter != nil {
    							c.prompter.AppendHistory(command)
    						}
    					}
    				}
    				c.Evaluate(input)
    				input = ""
    			}
    		}
    	}
    }

    该方法首先创建一个scheduler通道,用于进程间的通信。接着使用goroutine的方式创建一个线程来监听用户在控制台的输入。接着一个无线for循环来处理从scheduler传递过来用户的输入信息。首先通过从scheduler通道读取数据line,然后做一些数据合法性验证。接着调用console的Evaluate方法将用户输入的命令交给前面的jsre处理(最终调用Do方法向evalQueue通道发送数据),最终前面初始化console时初始化的jsre用于接收用户输入命令的线程接收到数据,然后调用VM来处理,并将结果返回在控制台显示。

    至此,整个以太坊就启动起来了,完成了相关AccountManager的初始化、P2P Node的启动、RPC Server启动以及控制台交互的事件监听。后续我们将分析P2P网络的启动、转账的具体实现等等。对于分析的流程,可能有些地方有错误,麻烦大家指出,感谢~

    展开全文
  • 这本以太坊源码分析pdf详细介绍了以太坊的源码内容,结构主体,以及怎样使用,希望对各位朋友有用
  • 以太坊源码分析

    2021-05-16 18:39:32
    以太坊是互联网新时代的基础: 内建货币与支付。 用户拥有个人数据主权,且不会被各类应用监听或窃取数据。 人人都有权使用开放金融系统。 基于中立且开源的基础架构,不受任何组织或个人控制。 以太坊主网于 2015 ...
  • 区块链 浏览器(以太坊源码, 用于区块链浏览器的源码
  • 以太坊目标 以太坊的目标是基于区块链技术打造一个运行智能合约的去中心化平台。...以太坊源码分析(1)go-ethereum的设计思路及模块组织形式 以太坊源码解读(1)以太坊核心概念 以太坊网络架构解析

    以太坊目标

    以太坊的目标是基于区块链技术打造一个运行智能合约的去中心化平台。

    以太坊核心概念

    在这里插入图片描述

    EVM
    EVM是以太坊中智能合约的运行环境。它不仅被沙箱封装起来,事实上它被完全隔离,也就是说运行在EVM内部的代码不能接触到网络、文件系统或者其它进程。甚至智能合约之间也只有有限的调用。

    EVM不是基于寄存器,而是基于栈的虚拟机。因此所有的计算都在一个被称为栈的区域执行。栈最大有1024个元素,每个元素256比特。对栈的访问只限于其顶端,无法只访问栈上指定深度的那个元素,在那之前必须要把指定深度之上的所有元素都从栈中移除才行。当然可以把栈上的元素放到storage或者memory中。

    存储,主存(Storage, Memory )
    每个账户有一块持久化内存区域被称为存储。其形式为key-value,key和value的长度均为256比特。在合约里,不能遍历账户的存储。相对于另外两种,存储的读操作相对来说开销较大,修改存储更甚。一个合约只能对它自己的存储进行读写。

    第二个内存区被称为主存。合约执行每次消息调用时,都有一块新的,被清除过的主存。主存可以以字节粒度寻址,但是读写粒度为32字节(256比特)。操作主存的开销随着其增长而变大(平方级别)。

    账户(Account)
    外部账户:存储以太币,被公钥-私钥对控制,其地址由公钥决定,没有代码,可通过创建和签名一笔交易从一个外部账户发送消息。
    合约账户:主要存储执行的合约代码,被存储在账户中的代码控制,每当合约账户收到消息,合约内部的代码就会被激活,允许它对内部存储进行读取/写入/发送其他消息和创建合约。

    交易(Transaction)
    交易是指从外部账户发出的签名数据包,包含交易接收者、用于确认发送者的签名、交易额度、要发送的数据、GasLimit、GasPrice。GasLimit通过需要支付的燃料来对计算步骤进行限制,防止合约代码出现指数型爆炸和无限循环。
    当一个交易的目标账户是一个合约账户,那么合约账户里的代码就会与交易里的数据(payload)一同执行。如果交易目标账户是零账户,代表要创建一个合约,交易的payload就是一个合约代码的ABI,ABI作为EVM字节码执行,输出真正的合约代码被永久储存。这个过程就是“部署合约”。

    Gas
    以太坊上的每笔交易都会被收取一定数量的gas,gas的目的是限制执行交易所需的工作量,同时为执行支付费用。当EVM执行交易时,gas将按照特定规则被逐渐消耗。
    gas price(gas价格,以太币计)是由交易创建者设置的,发送账户需要预付的交易费用 = gas price * gas amount。 如果执行结束还有gas剩余,这些gas将被返还给发送账户。
    无论执行到什么位置,一旦gas被耗尽(比如降为负值),将会触发一个out-of-gas异常。当前调用帧所做的所有状态修改都将被回滚。

    指令集
    EVM所支持的指令编码,EVM所有的指令都针对256位这个基本的数据单位进行操作。此外合约可以访问当前区块的相关属性,比如它的编号和时间戳。

    P2P网络
    以太坊分布式网络中的所有节点都地位平等,没有中心服务器。

    日志(Logs)
    在区块层面,可以用一种特殊的可索引的数据结构来存储数据。这个特性被称为日志,Solidity用它来实现事件。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在布隆过滤器(Bloom filter) 中,我们可以高效并且安全的搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

    以太坊的世界状态
    包括每一个账户的余额、合约代码、账户的储存、Nonce值。
    以太坊的状态转换是指在一个交易发生时,以太坊从一个正确的状态S转到下一个正确的状态S’的转换过程。

    挖矿(Mine)
    以太坊网络通过工作量证明算法来保证网络的安全运行。

    共识算法的两种实现
    以太坊共识算法对外暴露的接口是Engine接口,共识算法由两种实现体:
    1、Ethash算法(POW):基础运算能力,是目前以太坊基于POW工作量证明的一个共识引擎(也叫挖矿算法)。
    2、Clique算法(POS):基于“同行”认证,网络中的每一个区块是由某一个认证节点进行认证的,其他节点仅需要验证认证信息来判断该区块是否合法。
    也就是说,以太坊的共识算法是系统提供的一个接口,实现可插拔式的,只要实现了共识算法就可以被引用。Ethash和Clique都是Engine接口的两种实现。

    以太坊模型

    以太坊本质是一个基于交易的状态机(transaction-based state machine),以太坊的状态中有百万个交易,
    这些交易被打包到一个区块中,每个区块都和之前的区块链接起来,形成一个反向链表,所以叫区块链。在区块链的基础上增加了智能合约打造出以太坊。

    为了让一笔交易被认为是有效的,它必须要经过一个验证过程,也就是挖矿(Mine)。 任何一个以太坊网络上的矿工都可以尝试创建和验证区块,如果一个区块被认为是有效的,并且是最快完成验证的,那么就会添加到主链上,主链是以太坊网络上最长的一条链。如果同时有多个矿工打包了一个区块,加上区块在网络中传播需要一定时间,难免会产生多条路径,就是所谓的分叉。

    为了防止多条链的产生,以太坊使用了 GHOST 协议 (Greedy Heaviest Observed Subtree),也就是选择一条完成计算最多的路径,区块号越大, 路径就越长,说明挖矿消耗的算力越多。
    因为成功证实了一个新区块会得到一定以太币的奖励,所以从经济学和博弈论的角度,选择主链是最优的。

    以太坊的架构

    以太坊的架构设计可以简单的分为三个层次,协议层接口层应用层。而协议层又可以分为网络层和存储层。
    从技术角度看,协议层主要包括P2P网络通信,分布式算法,加密签名和数据存储技术。数据存储底层,比特币和以太坊都选用了Google开源的LevelDB数据库。
    接口层与协议层完全分离,除了交易时与协议层进行交互,保证开发各种基于区块链的应用层业务不受约束,包括分布式存储业务,机器学习,物联网等。
    应用层主要是从区块链自身的特性出发,在不引用第三方机构的前提下,提供去中心化,不可篡改,安全可靠的场景应用。 主要包括金融服务,征信和权属管理,资源共享,投资管理以及物联网和供应链等。

    以太坊核心数据结构

    1. 区块(Block)是以太坊的核心数据结构之一,Block包含Header和Body两部分。
    2. Blockchain和HeaderChain, Blockchain管理所有的Block, 让其组成一个单向链表。Headerchain管理所有的Header,也形成一个单向链表, Headerchain是Blockchain里面的一部分。
    3. Transaction是Body的重要数据结构,一个交易就是被外部拥有账户生成的加密签名的一段指令,序列化,然后提交给区块链。
    4. 以太坊的数据库体系-Merkle-Patricia Trie(MPT), 它是由一系列节点组成的二叉树,在树底包含了源数据的大量叶子节点, 父节点是两个子节点的Hash值,一直到根节点。

    go ethereum 源码的目录结构

    accounts        	实现了一个高等级的以太坊账户管理
    bmt			二进制的默克尔树的实现
    build			主要是编译和构建的一些脚本和配置
    cmd			命令行工具,又分了很多的命令行工具,下面一个一个介绍
    	/abigen		Source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages
    	/bootnode	启动一个仅仅实现网络发现的节点
    	/evm		以太坊虚拟机的开发工具, 用来提供一个可配置的,受隔离的代码调试环境
    	/faucet		
    	/geth		以太坊命令行客户端,最重要的一个工具
    	/p2psim		提供了一个工具来模拟http的API
    	/puppeth	创建一个新的以太坊网络的向导
    	/rlpdump 	提供了一个RLP数据的格式化输出
    	/swarm		swarm网络的接入点
    	/util		提供了一些公共的工具
    	/wnode		这是一个简单的Whisper节点。 它可以用作独立的引导节点。此外,可以用于不同的测试和诊断目的。
    common			提供了一些公共的工具类
    compression		Package rle implements the run-length encoding used for Ethereum data.
    consensus		提供了以太坊的一些共识算法,比如ethhash, clique(proof-of-authority)
    console			console类
    contracts	
    core			以太坊的核心数据结构和算法(虚拟机,状态,区块链,布隆过滤器)
    crypto			加密和hash算法,
    eth			实现了以太坊的协议
    ethclient		提供了以太坊的RPC客户端
    ethdb			eth的数据库(包括实际使用的leveldb和供测试使用的内存数据库)
    ethstats		提供网络状态的报告
    event			处理实时的事件
    les			实现了以太坊的轻量级协议子集
    light			实现为以太坊轻量级客户端提供按需检索的功能
    log			提供对人机都友好的日志信息
    metrics			提供磁盘计数器
    miner			提供以太坊的区块创建和挖矿
    mobile			移动端使用的一些warpper
    node			以太坊的多种类型的节点
    p2p			以太坊p2p网络协议
    rlp			以太坊序列化处理
    rpc			远程方法调用
    swarm			swarm网络处理
    tests			测试
    trie			以太坊重要的数据结构Package trie implements Merkle Patricia Tries.
    whisper			提供了whisper节点的协议。
    

    参考:
    go-ethereum-code-analysis
    以太坊源码分析(1)go-ethereum的设计思路及模块组织形式
    以太坊源码解读(1)以太坊核心概念

    以太坊网络架构解析

    展开全文
  • 以太坊源码记录.pdf

    2020-03-26 15:24:13
    偶然发现之前自己学习以太坊源码时候整理的思维导图,现在也发出来,有大佬指点的话更好呀,毕竟一个人的理解不比大帮子,第一次发,紧张
  • 以太坊源码解析 - 交易源码分析

    千次阅读 2018-06-02 12:41:53
    以太坊交易基本流程: 完整流程分为以下几个步骤: 发起交易:指定目标地址和交易金额,以及需要的gas/gaslimit 交易签名:使用账户私钥对交易进行签名 提交交易:把交易加入到交易缓冲池txpool中(会先对交易...
  • go-ethereum以太坊源码解析完整版

    万次阅读 2018-06-01 11:14:46
    go-ethereum-code-analysis 目录 go-ethereum代码阅读环境搭建 以太坊黄皮书 符号索引 rlp源码解析 trie源码分析 ethdb源码分析 rpc源码分析 p2p源码分析 ...以太坊的trie树管理 回滚等操作 state源码...
  • 以太坊源码分析报告
  • 以太坊源码分析(7)Ethereum 资源分享

    千次阅读 2018-05-13 23:28:55
    以太坊源码阅读 ]( http://www.cnblogs.com/baizx/category/1011749.html ) - [ PoW模式下交易平均要35秒? ]( http://ethfans.org/posts/current-dynamics-of-transaction-inclusion-on-ethereum ) ## Tutorials -...
  • 环境前准备 Ubuntu16.04 64位系统 ...此处需要注意,因为go的版本会造成后面以太坊环境测试,因此此处提供可行的一个go版本(go1.11.2)。 1. 进入/usr/local目录 cd /usr/local 2. 安装go1.11.2版本 wget h...
  • 以太坊源码目录结构分析

    千次阅读 2018-05-09 10:28:37
    p2p/enr 实现EIP-778中的以太坊节点记录 p2p/nat 提供网络端口映射协议的权限 p2p/netutil 网络包拓展 p2p/protocols p2p拓展 p2p/simulations 实现模拟p2p网络 p2p/simulations/adapters – p2p/...
  • 以太坊_阅读 以太坊源码研读 go-ethereum-read:以太坊源码注释 目录 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 一种。 b。 c。还在陆续更新中..... ...... 更多区块链技术文章请访问
  • 学习了一下以太坊如何处理区块到达时间的。这里记录一下。 消息中ReceivedAt的写入逻辑 一切的起点是p2p模块的server.run()函数 从这里开始,geth启动了p2p服务器。 func (srv *Server) run(dialstate dialer) { ...
  • 前面几节都在分析以太坊的通信协议,怎么广播,怎么同步,怎么下载。这一节讲讲以太坊的核心模块BlockChain,也就是以太坊的区块链。1,BlockChain的初始化Ethereum服务初始化的时候会调用core.SetupGenesisBlock来...
  • 以太坊源码学习(一)

    2018-05-23 10:06:49
    转载自:...虽然如此,还是可以从geth仓库的第一个commit开始,这时的代码比较少,但是以太坊核心的雏形已经隐隐可见,阅读代码可以加深理解以太坊的模块组成,揣测设计的想法和思路。项目结构去...
  • 以太坊是一个平台,它上面提供各种模块让用户来搭建应用,如果将搭建应用比作造房子,那么以太坊就提供了墙面、屋顶、地板等模块,用户只需像搭积木一样把房子搭起来,因此在以太坊上建立应用的成本和速度都大大改善...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 7,922
精华内容 3,168
关键字:

以太坊源码