兄弟连Go语言+区块链培训以太坊源码分析(18)以太坊交易执行分析

Delete_88c3 · · 1322 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

兄弟连Go语言+区块链培训学院院长尹成资深区块链技术专家:毕业于清华大学,曾担任Google算法工程师,微软区块链领域全球最具价值专家,微软Tech.Ed大会金牌讲师。精通C/C++、Python、Go语言、Sicikit-Learn与TensorFlow。拥有15年编程经验与5年的教学经验,资深软件架构师,Intel软件技术专家,著名技术专家,具备多年的世界顶尖IT公司微软谷歌的工作经验。具备多年的软件编程经验与讲师授课经历,并在人机交互、教育、信息安全、广告、区块链系统开发诸多产品。具备深厚的项目管理经验以及研发经验,拥有两项人工智能发明专利,与开发电子货币部署到微软WindowsAzure的实战经验。教学讲解深入浅出,使学员能够做到学以致用。

#以太坊交易执行分析

在这里,将其整体串起来,从state_processor.Process函数开始,归纳一下其所作的处理。

##1 Process

Process 根据以太坊规则运行交易信息来对statedb进行状态改变,以及奖励挖矿者或者是其他的叔父节点。

Process返回执行过程中累计的收据和日志,并返回过程中使用的Gas。 如果由于Gas不足而导致任何交易执行失败,将返回错误。

**处理逻辑:**

~~~

1. 定义及初始化收据、耗费的gas、区块头、日志、gas池等变量;

2. 如果是DAO事件硬分叉相关的处理,则调用misc.ApplyDAOHardFork(statedb)执行处理;

3. 对区块中的每一个交易,进行迭代处理;处理逻辑:

a. 对当前交易做预处理,设置交易的hash、索引、区块hash,供EVM发布新的状态日志使用;

b. 执行ApplyTransaction,获取收据;

c. 若上一步出错,中断整个Process,返回错误;

d. 若正常,累积记录收据及日志。循环进入下一个交易的处理。

4. 调用共识模块做Finalize处理;

5. 返回所有的收据、日志、总共使用的gas。

~~~

##2 ApplyTransaction(1.3.b )

ApplyTransaction尝试将交易应用于给定的状态数据库,并使用输入参数作为其环境。

它返回交易的收据,使用的Gas和错误,如果交易失败,表明块是无效的。

**处理逻辑:**

~~~

1. 将types.Transaction结构变量转为core.Message对象;这过程中会对发送者做签名验证,并获得发送者的地址缓存起来;

2. 创建新的上下文(Context),此上下文将在EVM 环境(EVM environment)中使用;上下文中包含msg,区块头、区块指针、作者(挖矿者、获益者);

3. 创建新的EVM environment,其中包括了交易相关的所有信息以及调用机制;

4. ApplyMessage, 将交易应用于当前的状态中,也就是执行状态转换,新的状态包含在环境对象中;得到执行结果以及花费的gas;

5. 判断是否分叉情况( `config.IsByzantium(header.Number)` ),如果不是,获取当前的statedb的状态树根哈希;

6. 创建一个收据, 用来存储中间状态的root, 以及交易使用的gas;

7. 如果是创建合约的交易,那么我们把创建地址存储到收据里面;

8. 拿到所有的日志并创建日志的布隆过滤器;返回。

~~~

##3 ApplyMessage(2.4)

ApplyMessage将交易应用于当前的状态中,代码里就是创建了一个StateTransition然后调用其TransitionDb()方法。

ApplyMessage返回由任何EVM执行(如果发生)返回的字节(但这个返回值在ApplyTransaction中被忽略了),

使用的Gas(包括Gas退款),如果失败则返回错误。 一个错误总是表示一个核心错误,

意味着这个消息对于这个特定的状态将总是失败,并且永远不会在一个块中被接受。

##4 StateTransition.TransitionDb()

~~~

1. 预检查,出错则函数返回;

a. 检查交易的Nonce值是否合规;

b. buyGas:根据发送者定的gaslimit和GasPrice,从发送者余额中扣除以太币;从区块gas池中减掉本次gas;并对运行环境做好更新;

2. 支付固定费用 intrinsic gas;

3. 如果是合约创建, 那么调用evm的Create方法创建新的合约,使用交易的data作为新合约的部署代码;

4. 否则不是合约创建,增加发送者的Nonce值,然后调用evm.Call执行交易;

5. 计算并执行退款,将退回的gas对应的以太币退回给交易发送者。

~~~

###4.3 evm.Create创建新的合约

~~~

1. 检查执行深度,若超过params.CallCreateDepth(即1024)就出错返回;刚开始的执行深度为0,肯定继续往下执行;

2. 检查是否可执行转账,即检查账户余额是否≥要转账的数额;

3. 发送者Nonce加1;

4. 创建合约地址并获取hash,若该合约地址已存在,或不合法(空),则出错返回;

5. 保存statedb快照,然后根据合约地址创建账户;

6. 执行转账evm.Transfer(在statedb中,将value所代表的以太币从发送者账户转到新合约账户);

7. 根据发送者、前面创建的合约账户,转账的钱,已用的gas创建并初始化合约;将交易的data作为合约的代码;

8. 运行前一步创建的合约

9. 判断运行结果是否有错误。如果合约成功运行并且没有错误返回,则计算存储返回数据所需的GAS。 如果由于没有足够的GAS而导致返回值不能被存储则设置错误,并通过下面的错误检查条件来处理。

10. 若EVM返回错误或上述存储返回值出现错误,则回滚到快照的状态,并且消耗完剩下的所有gas。

~~~

###4.4 evm.Call执行交易

Call方法, 无论我们转账或者是执行合约代码都会调用到这里, 同时合约里面的call指令也会执行到这里。

Call方法和evm.Create的逻辑类似,但少了一些步骤。

~~~

1. 检查是否允许递归执行以及执行深度,若深度超过params.CallCreateDepth(即1024)就出错返回;

2. 检查是否可执行转账,即检查账户余额是否≥要转账的数额;

3. 保存statedb快照,创建接收者账户;

4. 如果接收者在statedb中尚不存在,则执行precompiles预编译,与编译结果为nil时出错返回;无错误则在statedb中创建接收者账户;

5. 执行转账;

6. 根据发送者、接收者,转账的钱,已用的gas创建并初始化合约;将交易的data作为合约的代码;

7. 运行前一步创建的合约

8. 若EVM返回错误,则回滚到快照的状态,并且消耗完剩下的所有gas。

~~~

虚拟机中合约的执行另行分析。

### eth源码交易发送接收,校验存储分析:

```

创建合约指的是将合约部署到区块链上,这也是通过发送交易来实现。在创建合约的交易中,to字段要留空不填,在data字段中指定合约的二进制代码,

from字段是交易的发送者也是合约的创建者。

执行合约的交易

调用合约中的方法,需要将交易的to字段指定为要调用的合约的地址,通过data字段指定要调用的方法以及向该方法传递的参数。

所有对账户的变动操作都会先提交到stateDB里面,这个类似一个行为数据库,或者是缓存,最终执行需要提交到底层的数据库当中,底层数据库是levelDB(K,V数据库)

core/interface.go定义了stateDB的接口

ProtocolManager主要成员包括:

peertSet{}类型成员用来缓存相邻个体列表,peer{}表示网络中的一个远端个体。

通过各种通道(chan)和事件订阅(subscription)的方式,接收和发送包括交易和区块在内的数据更新。当然在应用中,订阅也往往利用通道来实现事件通知。

ProtocolManager用到的这些通道的另一端,可能是其他的个体peer,也可能是系统内单例的数据源比如txPool,或者是事件订阅的管理者比如event.Mux。

Fetcher类型成员累积所有其他个体发送来的有关新数据的宣布消息,并在自身对照后,安排相应的获取请求。

Downloader类型成员负责所有向相邻个体主动发起的同步流程。

func(pm *ProtocolManager) Start()

以上这四段相对独立的业务流程的逻辑分别是:

1.广播新出现的交易对象。txBroadcastLoop()会在txCh通道的收端持续等待,一旦接收到有关新交易的事件,会立即调用BroadcastTx()函数广播给那些尚无该交易对象的相邻个体。

2.广播新挖掘出的区块。minedBroadcastLoop()持续等待本个体的新挖掘出区块事件,然后立即广播给需要的相邻个体。当不再订阅新挖掘区块事件时,这个函数才会结束等待并返回。很有意思的是,在收到新挖掘出区块事件后,minedBroadcastLoop()会连续调用两次BroadcastBlock(),两次调用仅仅一个bool型参数@propagate不一样,当该参数为true时,会将整个新区块依次发给相邻区块中的一小部分;而当其为false时,仅仅将新区块的Hash值和Number发送给所有相邻列表。

3.定时与相邻个体进行区块全链的强制同步。syncer()首先启动fetcher成员,然后进入一个无限循环,每次循环中都会向相邻peer列表中“最优”的那个peer作一次区块全链同步。发起上述同步的理由分两种:如果有新登记(加入)的相邻个体,则在整个peer列表数目大于5时,发起之;如果没有新peer到达,则以10s为间隔定时的发起之。这里所谓"最优"指的是peer中所维护区块链的TotalDifficulty(td)最高,由于Td是全链中从创世块到最新头块的Difficulty值总和,所以Td值最高就意味着它的区块链是最新的,跟这样的peer作区块全链同步,显然改动量是最小的,此即"最优"。

4.将新出现的交易对象均匀的同步给相邻个体。txsyncLoop()主体也是一个无限循环,它的逻辑稍微复杂一些:首先有一个数据类型txsync{p, txs},包含peer和tx列表;通道txsyncCh用来接收txsync{}对象;txsyncLoop()每次循环时,如果从通道txsyncCh中收到新数据,则将它存入一个本地map[]结构,k为peer.ID,v为txsync{},并将这组tx对象发送给这个peer;每次向peer发送tx对象的上限数目100*1024,如果txsync{}对象中有剩余tx,则该txsync{}对象继续存入map[]并更新tx数目;如果本次循环没有新到达txsync{},则从map[]结构中随机找出一个txsync对象,将其中的tx组发送给相应的peer,重复以上循环。

以上四段流程就是ProtocolManager向相邻peer主动发起的通信过程。尽管上述各函数细节从文字阅读起来容易模糊,不过最重要的内容还是值得留意下的:本个体(peer)向其他peer主动发起的通信中,按照数据类型可分两类:交易tx和区块block;而按照通信方式划分,亦可分为广播新的单个数据和同步一组同类型数据,这样简单的两两配对,便可组成上述四段流程。

在上文的介绍中,出现了多处有关p2p通信协议的结构类型,比如eth.peer,p2p.Peer,Server等等。这里不妨对这些p2p通信协议族的结构一并作个总解。以太坊中用到的p2p通信协议族的结构类型,大致可分为三层:

第一层处于pkg eth中,可以直接被eth.Ethereum,eth.ProtocolManager等顶层管理模块使用,在类型声明上也明显考虑了eth.Ethereum的使用特点。典型的有eth.peer{}, eth.peerSet{},其中peerSet是peer的集合类型,而eth.peer代表了远端通信对象和其所有通信操作,它封装更底层的p2p.Peer对象以及读写通道等。

第二层属于pkg p2p,可认为是泛化的p2p通信结构,比较典型的结构类型包括代表远端通信对象的p2p.Peer{}, 封装自更底层连接对象的conn{},通信用通道对象protoRW{}, 以及启动监听、处理新加入连接或断开连接的Server{}。这一层中,各种数据类型的界限比较清晰,尽量不出现揉杂的情况,这也是泛化结构的需求。值得关注的是p2p.Protocol{},它应该是针对上层应用特意开辟的类型,主要作用包括容纳应用程序所要求的回调函数等,并通过p2p.Server{}在新连接建立后,将其传递给通信对象peer。从这个类型所起的作用来看,命名为Protocol还是比较贴切的,尽管不应将其与TCP/IP协议等既有概念混淆。

第三层处于golang自带的网络代码包中,也可分为两部分:第一部分pkg net,包括代表网络连接的接口,代表网络地址的以及它们的实现类;第二部分pkg syscall,包括更底层的网络相关系统调用类等,可视为封装了网络层(IP)和传输层(TCP)协议的系统实现。

```

```

Receiptroot我们刚刚在区块头有看到,那他具体包含的是什么呢?它是一个交易的结果,主要包括了poststate,交易所花费的gas,bloom和logs

blockchain无结构化查询需求,仅hash查询,key/value数据库最方便,底层用levelDB存储,性能好

stateDB用来存储世界状态

Core/state/statedb.go

注意:1. StateDB完整记录Transaction的执行情况; 2. StateDB的重点是StateObjects; 3. StateDB中的 stateObjects,Account的Address为 key,记录其Balance、nonce、code、codeHash ,以及tire中的 {string:Hash}等信息;

所有的结构凑明朗了,那具体的验证过程是怎么样的呢

Core/state_processor.go

Core/state_transition.go

Core/block_validator.go

StateProcessor 1. 调用StateTransition,验证(执行)Transaction; 2. 计算Gas、Recipt、Uncle Reward

StateTransition

1. 验证(执行)Transaction;

3. 扣除transaction.data.payload计算数据所需要消耗的gas;

4. 在vm中执行code(生成contract or 执行contract);vm执 行过程中,其gas会被自动消耗。如果gas不足,vm会自 选退出;

5. 将多余的gas退回到sender.balance中;

6. 将消耗的gas换成balance加到当前env.Coinbase()中;

BlockValidator

1. 验证UsedGas

2. 验证Bloom

3. 验证receiptSha

4. 验证stateDB.IntermediateRoot

/core/vm/evm.go

交易的转帐操作由Context对象中的TransferFunc类型函数来实现,类似的函数类型,还有CanTransferFunc, 和GetHashFunc。

core/vm/contract.go

合约是evm用来执行指令的结构体

入口:/cmd/geth/main.go/main

```

#EVM分析

>EVM不能被重用,非线程安全

Context结构体:为EVM提供辅助信息。一旦提供,不应更改。

~~~

// Context 为EVM提供辅助信息。一旦提供,不应更改。

type Context struct {

    // CanTransfer 返回 账户是否拥有足够的以太币以执行转账 CanTransfer CanTransferFunc

    // Transfer 转账函数,将以太币从一个账户转到另一个账户

    Transfer TransferFunc

    // GetHash 返回n对应的哈希

    GetHash GetHashFunc

    // Message information

    Origin common.Address // Provides information for ORIGIN

    GasPrice *big.Int // Provides information for GASPRICE

    // Block information

    Coinbase common.Address // Provides information for COINBASE

    GasLimit uint64 // Provides information for GASLIMIT

    BlockNumber *big.Int // Provides information for NUMBER

    Time *big.Int // Provides information for TIME

    Difficulty *big.Int // Provides information for DIFFICULTY

}

~~~

> state_processor.Process开始执行交易处理,就是在那里为入口进入到evm的执行的,具体见[core-state-process-analysis.md](core-state-process-analysis.md)

##EVM的实现

以太坊的EVM整个完全是自己实现的,能够直接执行Solidity字节码,没有使用任何第三方运行时。

运行过程是同步的,没有启用go协程。

1. evm最终是调用Interpreter运行字节码;

2. Interpreter.go实现运行处理;解析出操作码后,通过JumpTable获取操作码对应的函数运行,并维护pc计数器、处理返回值等;

3. jump_table.go定义了操作码的跳转映射;

4. instructions.go实现每一个操作码的具体的处理;

5. opcodes.go中定义了操作码常量

对于EVM的测试,以太坊将测试代码放在了core\vm\runtime目录下,提供了供测试用的运行时及测试用例。

测试用例的示例如:

~~~

func TestExecute(t *testing.T) {

    ret, _, err := Execute([]byte{

        byte(vm.PUSH1), 10,

        byte(vm.PUSH1), 0,

        byte(vm.MSTORE),

        byte(vm.PUSH1), 32,

        byte(vm.PUSH1), 0,

        byte(vm.RETURN),

    }, nil, nil)

    if err != nil {

        t.Fatal("didn't expect error", err)

    }

    num := new(big.Int).SetBytes(ret)

    if num.Cmp(big.NewInt(10)) != 0 {

        t.Error("Expected 10, got", num)

    }

}

~~~

  比特币没赶上?以太币没有买?你错过了成为了百万富翁的梦但是不要错失成为创造者的机会!!!7月7日起每晚8点-9点半兄弟连区块链学院正式开课:http://www.ydma.cn/open/course/16【清华学霸携全球区块链大赛冠军团队】带你实战区块链开发!!!


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1322 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传