以太坊探究:ETH交易部分分析

尚力财经 222 0

事务结构在core/types/transaction.go中定义:

这个atomic是go语言的包同步/原子,用来实现原子操作。在这个结构中,数据是数据字段,其他三个是缓存。接下来是计算hash的函数:

尚力财经小编2022 在计算hash之前,会先从缓存tx.hash中获取,如果获取了就直接返回值。否则使用rlpHash计算:

hash计算如下:rlpEncode事务的tx.data(定义在:core/types/transaction . go)

然后计算为KEKE。即txhash=kekcak256 (RLP编码(tx.data))

事务,数据是txdata类型,在同一个文件中定义, 它详细规定了交易的具体字段:

这些字段的详细解释如下:AccountNonce:本次交易发送方发送的交易数(防止重放攻击)价格:燃气价格本次交易的燃气限额:本次交易允许消耗的最大燃气量接收方:本次交易接收方的地址,如果该字段为零, 那么这个交易就是一个“契约创建”类型的交易金额:交易转移的以太币金额,单位为weiPayload:交易可以承载的数据在不同类型的交易中有不同的含义。 V R S:交易的签名数据

我们会发现交易中没有包含发送方的地址,因为这个地址已经包含在签名信息中了,后面我们会分析相关代码。另外,传输的数据格式是json。因此,在本文档中,还定义了事务的json数据结构和相关的转换函数。

函数有:MarshalJSON()和UnmarshlJSON(),它们会调用core/types/gen_tx_json.go文件中的同名函数来转换内部和外部数据类型。

事务存储

事务的获取和存储函数是:Get/WriteTXLookupEntries,在core/database _ util.go中定义

对于每个传入的块,该函数会读取块中的每个事务分别进行处理。首先,创建一个数据类型为txLookupEntry的条目。包括内容块的散列、块号和事务索引(事务在块中的位置),然后rlp将该条目编码为存储在数据库中的值。部分key类似于块存储,其组成结构是事务前缀事务hash。

该函数的调用主要在core/blockchain.go中,比如WriteBlockAndState()会将块写入数据库,处理body部分时需要分别处理每个事务。而WriteBlockAndState是由miner/worker.go中的wait函数调用的mainer/worker.go中的newWorker函数会调用worker.wait()。

事务类型

web 3 . eth . send transaction(transaction object[,callback]) (web3.js在internal/jsre/deps)。

参数是一个对象。如果在发送事务时指定了不同的字段,区块链节点可以识别相应的事务类型。

转让交易:

?转账是最简单的交易,其中转账是指将以太币从一个账户发送到另一个账户。发送转账交易时,您只需指定汇款人、收款人和转账金额。使用web3.js发送转账交易应该是这样的:

值为转账的以太币数,单位为魏,对应源代码中的金额字段。To对应于源代码中的接收方

以创建合同事务:

?创建契约是指将契约部署到区块链,这也是通过发送事务来实现的。在创建合同的交易中,to字段应该为空,并且应该在数据字段中指定合同的二进制代码。from字段是事务的发送者和合同的创建者。

数据字段对应于源代码中的有效载荷字段。

执行契约事务:

调用契约中的方法,需要指定事务的to字段作为要调用的契约的地址,并通过数据字段指定要调用的方法和传递给方法的参数。

数据字段需要特殊的编码规则。具体请参考?以太坊契约ABI(自己拼接字段不方便,容易出错,所以常用打包的SDK(如web3.js)调用契约)。

事务执行

?按照以太坊的架构设计,事务执行大致可以分为两层:第一层在虚拟机之外,包括在执行前将事务类型转换成消息,创建虚拟机(EVM)对象,计算一些气体消耗,事务执行后创建Receipt对象并返回。第二层在虚拟机中,包括执行调用、创建契约和执行契约的指令数组。虚拟机外:

执行tx的入口函数是Process()函数,在core/state _ processor . go .

?Process()函数的核心是一个for循环,逐个遍历并执行块中的所有tx。的具体执行函数是同一个go文件中的ApplyTransaction()函数,每次执行tx都返回一个Receipt对象。收据结构的声明如下(core/types/Receipt . go):

?Receipt中有一个Log类型的数组,其中每个Log对象记录Tx中的一小步。因此,每个tx的执行结果由一个Receipt对象表示;更详细的内容由一组日志对象记录。这个日志数组非常重要。例如,在不同以太坊节点的相互同步过程中,要同步的块的日志数组有助于验证同步中接收到的块是否正确和完整,所以会分别同步(传输)。

收据的PostState保存创建收据对象时整个块中所有“帐户”的当前状态。以太坊使用stateObject来表示Account account,Account可以传递值和执行tx,它唯一的标识符是一个地址类型变量。收据。PostState是当时块中所有stateObject对象的RLP哈希值。

Bloom type是以太坊中实现的256位长Bloom过滤器。布鲁姆滤镜概念定义可见?维基百科,http://blog.csdn.net/jiaomeng/article/details/1495500它可以用来快速验证一个新收到的对象是否在大量已知对象中。这里,接收的Bloom用于验证给定的日志是否在现有的接收日志数组中。

?我们来看看state processor . apply transaction()的具体实现。其基本流程如下:

?ApplyTransaction()首先根据输入参数封装一个消息对象和一个EVM对象,然后添加一个传入的GasPool类型变量来执行core/state_transition.go中的ApplyMessage(),反过来这个函数调用同一个go文件中的TransitionDb()函数来完成tx的执行。TransitionDb()返回后,创建一个Receipt receipt对象,最后返回Recetip对象和整个tx执行过程中消耗的气体数量。

GasPool对象在一个块开始执行时创建,并在该块中所有tx执行期间共享。TX的执行可以被视为“全局”存储对象;Message由本次要执行的tx对象转换而来,携带沉淀tx的(transfer)transfer的地址,属于要处理的数据对象;EVM作为以太坊世界中的虚拟机,也是这个tx的实际执行人,完成转移和契约的相关操作。

我们来仔细看看transitio indb()(/core/state _ transition . go)的执行过程。假设有一个StateTransition对象st,其成员变量initialGas表示初始可用气量,Gas表示立即可用气量,初始值都为0,那么st.TransitionDb()可以通过以下步骤展开:

首先执行preCheck()函数检查:1 .交易中的随机数和账户随机数是否相同。2.检查气值是否合适(=64 )

买气。 先从交易的(转账)转让方账户中扣除一笔乙醚,费用等于tx . data . gas limit * tx . data . price;同时,ST . initial gas=ST . gas=tx . data . gas limit;Gas pool)gp-=ST . Gas . tx-固有气体的计算固有气体消耗量。分为两部分,每个tx的预置消耗,这个消耗因tx是否包含(传送)被传送方地址而略有不同;以及tx.data.Payload的气体消耗,有效载荷类型为[]字节,其固有消耗取决于[]字节中非零字节和零字节的长度。最后,执行st.gas -=intrinsicGasEVM。如果交易(转账)的地址(tx.data.Recipient)为空,调用EVM的Create()函数,即contractCreation否则,调用Call()函数。无论哪个函数返回,update st.gas计算本次执行交易的实际用气量:所需气体=ST . initial Gas-ST . Gas报销气体。由两部分组成:一是将剩余的st.gas转化为乙醚,返回交易转让方的账户;然后,基于所需气体的实际消耗,系统提供一些补偿,并且数量是退还的气体。由refulGas转换的乙醚将立即添加到转让方的账户中(transfer),ST. Gas=refulgas,gp=st.gas,即剩余的气加上系统补偿的气,将一起放入GasPool,以供后续交易执行。奖励方块的挖掘者:系统在方块作者即挖掘者的账户上增加一笔等于st.data,price *(ST . initial gas-ST . gas)的金额。请注意,在步骤5中,退款气体被添加到st.Gas中,因此对应于此奖励的气体量少于交易所需气体的实际消耗量。

从上面可以看出,除了在步骤3中执行EVM功能之外,每隔一个步骤都关注气体消耗。

第五步的退款机制很有意思。设置的目的是什么?到目前为止,我只能理解为可以避免交易执行过程中对汽油的过度消耗,但要全面准确的理解还需要时间。

第六步是奖励机制。没什么好说的。

以太坊中的每一个交易(tx)对象在放入块中时都经过数字签名,以便在后续的传输和处理中随时验证tx是否被篡改。以太坊采用的数字签名是椭圆曲线数字签名算法(ECDSA)。与基于大素数分解的RSA数字签名算法相比,ECDSA可以提供相同的安全级别(以比特为单位),只需要更短的公钥。这里应该注意的是,tx的传送传输者(发送者)的地址是在tx对象的ECDSA签名计算中使用的公钥。

以太坊中数字签名计算过程产生的签名是一个长度为65字节的字节数组,被切成三段放入tx。前32个字节赋给成员变量R,然后32个字节赋给S,1个字节赋给V,当然由于R,S,V的声明类型都是*大。Int,上面的赋值

当需要恢复tx对象的转出方地址时(比如需要执行交易时),以太坊会先从tx的签名中恢复公钥,然后将公钥转换成常用的。地址类型地址。在签名被tx对象的三个成员变量R,S,V S,V转换成字节数组[]byte后,

Ethereum为此定义了一个接口Signer,用于挂载签名,恢复公钥,哈希tx对象等。该接口在:/core/types/transaction _ signing . go的

中定义,该接口主要用于恢复发送地址、生成签名格式、生成事务哈希、验证等。

生成数字签名的函数称为SignTx(),其根在core/types/transaction _ signing . g中定义。o(mobile/accounts.go 中也有 SignTx,但是这个函数是调用 accounts/keystore/keystore.go中的 SignTX,最终又调用 types.SignTx),它会先调用其函数生成 signature, 然后调用tx.WithSignature()将 signature 分段赋值给 tx 的成员变量 R,S,V。

? Signer 接口中,恢复(提取?)转出方地址的函数为:Sender,Sender returns the address derived from the signature (V, R, S) using secp256k1。使用到的参数是:Signer 和 Transaction ,该函数定义在core/types/transaction_signing.go 中

? Sender()函数体中,signer.Sender()会从本次数字签名的签名字符串(signature)中恢复出公钥,并转化为 tx 的(转帐)转出方地址。此函数最终会调用同文件下的 recoverPlain 函数来进行恢复

在上文提到的 ApplyTransaction()实现中,Transaction 对象需要首先被转化成 Message接口,用到的AsMessage()函数即调用了此处的 Sender()。调用路径为: AsMessage->transaction_signing.Sender(两个参数的)–>sender(单个参数的) 在 Transaction 对象 tx 的转帐转出方地址被解析出以后,tx 就被完全转换成了Message 类型,可以提供给虚拟机 EVM 执行了。

虚拟机内:

? 每个交易(Transaction)带有两部分内容(参数)需要执行:

转帐,由转出方地址向转入方地址转帐一笔以太币 Ether;携带的[]byte 类型成员变量 Payload,其每一个 byte 都对应了一个单独虚拟机指令。这些内容都是由 EVM(Ethereum Virtual Machine)对象来完成 的。EVM 结构体是 Ethereum 虚拟机机制的核心,它与协同类的 UML 关系图如下:

? 其中 Context 结构体分别携带了 Transaction 的信息(GasPrice, GasLimit),Block 的信息(Number, Difficulty),以及转帐函数等,提供给 EVM;StateDB 接口是针对 state.StateDB 结构体设计的本地行为接口,可为 EVM 提供 statedb 的相关操作; Interpreter 结构体作为解释器,用来解释执行 EVM 中合约(Contract)的指令(Code)。

? 注意,EVM 中定义的成员变量 Context 和 StateDB, 仅仅声明了变量名而无类型,而变量名同时又是其类型名,在 Golang 中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如 EVM 调用 Context 中的 Transfer()。

交易的转帐操作由 Context 对象中的 TransferFunc 类型函数来实现,类似的函数类型,还有 CanTransferFunc, 和 GetHashFunc。这三个类型的函数变量 CanTransfer, Transfer, GetHash,在 Context 初始化时从外部传入,目前使用的均是一个本地实现。可见目前的转帐函数 Transfer()的逻辑非常简单,转帐的转出账户减掉一笔以太币,转入账户加上一笔以太币。由于 EVM 调用的 Transfer()函数实现完全由 Context 提供,所以,假设如果基于 Ethereum 平台开发,需要设计一种全新的“转帐”模式,那么只需写一个新的 Transfer()函数实现,在 Context 初始化时赋值即可。

有朋友或许会问,这里 Transfer()函数中对转出和转入账户的操作会立即生效么?万一两步操作之间有错误发生怎么办?答案是不会立即生效。StateDB 并不是真正的数据库, 只是一行为类似数据库的结构体。它在内部以 Trie 的数据结构来管理各个基于地址的账 户,可以理解成一个 cache;当该账户的信息有变化时,变化先存储在 Trie 中。仅当整个Block 要被插入到 BlockChain 时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。

合约的创建和赋值:

合约(Contract)是 EVM 用来执行(虚拟机)指令的结构体。Contract 的结构定义于:core/vm/contract.go 中,在这些成员变量里,caller 是转帐转出方地址(账户),self 是转入方地址,不过它们的类型都用接口 ContractRef 来表示;Code 是指令数组,其中每一个 byte 都对应于一个预定义的虚拟机指令;CodeHash 是 Code 的 RLP 哈希值;Input 是数据数组,是指令所操作的数据集合;Args 是参数。

? 有意思的是 self 这个变量,为什么转入方地址要被命名成 self 呢? Contract 实现了ContractRef 接口,返回的恰恰就是这个 self 地址。

func (c *Contract) Address() common.Address { return c.self.Address()

}

? 所以当 Contract 对象作为一个 ContractRef 接口出现时,它返回的地址就是它的 self地址。那什么时候 Contract 会被类型转换成 ContractRef 呢?当 Contract A 调用另一个Contract B 时,A 就会作为 B 的 caller 成员变量出现。Contract 可以调用 Contract,这就为系统在业务上的潜在扩展,提供了空间。

创建一个 Contract 对象时,重点关注对 self 的初始化,以及对 Code, CodeAddr 和Input 的赋值。

另外,StateDB 提供方法 SetCode(),可以将指令数组 Code 存储在某个 stateObject 对象中; 方法 GetCode(),可以从某个 stateObject 对象中读取已有的指令数组 Code。

123func (self *StateDB) SetCode(addr common.Address, code []byte) /func (self*StateDB) GetCode(addr common.Address) code []byte

? stateObject (core/state/state_object.go)是 Ethereum 里用来管理一个账户所有信息修改的结构体,它以一个 Address 类型变量为唯一标示符。StateDB 在内部用一个巨大的map 结构来管理这些 stateObject 对象。所有账户信息-包括 Ether 余额,指令数组 Code,该账户发起合约次数 nonce 等-它们发生的所有变化,会首先缓存到 StateDB 里的某个stateObject 里,然后在合适的时候,被 StateDB 一起提交到底层数据库。

? EVM(core/vm/evm.go)中 目前有五个函数可以创建并执行 Contract,按照作用和调用方式,可以分成两类:

? Create(), Call(): 二者均在 StateProcessor 的 ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。? CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐。考虑到与执行交易的相关性,这里着重探讨 Create()和 Call()。先来看 Call(),它用来处理(转帐)转入方地址不为空的情况:

Call()函数的逻辑可以简单分为以上 6 步。其中步骤(3)调用了转帐函数 Transfer(),转入账户 caller, 转出账户 addr;步骤(4)创建一个 Contract 对象,并初始化其成员变量 caller, self(addr), value 和 gas; 步骤(5)赋值 Contract 对象的 Code, CodeHash, CodeAddr 成员变量;步骤(6) 调用 run()函数执行该合约的指令,最后 Call()函数返回。相关代码可见:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {//如果设置了“禁用 call”,并且depth 正确,直接返回return nil, gas, nil}// Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) {//如果 call 的栈深度超过了预设值, 报错return nil, gas, ErrDepth}// Fail if we're trying to transfer more than the available balance if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {//检查发出账户是否有足够的钱(实际实现的函数定义在 core/evm.go/CanTransfer()中)但目前还不知道是怎么调用的 return nil, gas, ErrInsufficientBalance}var (to = AccountRef(addr)snapshot = evm.StateDB.Snapshot())if !evm.StateDB.Exist(addr) {//建立账户precompiles := PrecompiledContractsHomesteadif evm.ChainConfig().IsByzantium(evm.BlockNumber) { precompiles = PrecompiledContractsByzantium}if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {return nil, gas, nil}evm.StateDB.CreateAccount(addr)}evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)//转移// initialise a new contract and set the code that is to be used by the// E The contract is a scoped environment for this execution context// only.contract := NewContract(caller, to, value, gas)//建立合约contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr),evm.StateDB.GetCode(addr))ret, err = run(evm, snapshot, contract, input)// When an error was returned by the EVM or when setting the creation code// above we revert to the snapshot and consume any gas remaining. Additionally// when we're in homestead this also counts for code storage gas errors. if err != nil {evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted {contract.UseGas(contract.Gas)}}return ret, contract.Gas, err}? 因为此时(转帐)转入地址不为空,所以直接将入参 addr 初始化 Contract 对象的 self 地址,并可从 StateDB 中(其实是以 addr 标识的账户 stateObject 对象)读取出相关的 Code 和CodeHash 并赋值给 contract 的成员变量。注意,此时转入方地址参数 addr 同时亦被赋值予 contract.CodeAddr。

再来看看 EVM.Create(),它用来处理(转帐)转入方地址为空的情况。

与 Call()相比,Create()因为没有 Address 类型的入参 addr,其流程有几处明显不同:

? 步骤(3)中创建一个新地址 contractAddr,作为(转帐)转入方地址,亦作为Contract 的 self 地址;? 步骤(6)由于 contracrAddr 刚刚新建,db 中尚无与该地址相关的 Code 信息, 所以会将类型为[]byte 的入参 code,赋值予 Contract 对象的 Code 成员;? 步骤(8)将本次执行合约的返回结果,作为 contractAddr 所对应账户(stateObject 对象)的 Code 储存起来,以备下次调用。? 还有一点隐藏的比较深,Call()有一个入参 input 类型为[]byte,而 Create()有一个入参code 类型同样为[]byte,没有入参 input,它们之间有无关系?其实,它们来源都是Transaction 对象 tx 的成员变量 Payload!调用 EVM.Create()或 Call()的入口在StateTransition.TransitionDb()中,当 tx.Recipent 为空时,tx.data.Payload 被当作所创建Contract 的 Code;当 tx.Recipient 不为空时,tx.data.Payload 被当作 Contract 的 Input。

预编译合约

? EVM 中执行合约(指令)的函数是 run(),在 core/vm/evm.go 中其实现代码如下: 可见如果待执行的 Contract 对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr 为匹配项-那么它可以直接运行;没有经过预编译的 Contract,才会由Interpreter 解释执行。这里的”预编译”,可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要 Code,仅需 Input 即可。

在代码实现中,预编译合约只需实现两个方法 required()和 Run()即可,这两方法仅需一个入参 input。

1234567891011121314151617/ core/vm/contracts.gotype PrecompiledContract interface { RequiredGas(input []byte) uint64 Run(input []byte) ([]byte, error)}func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contrat) (ret []byte, err error) {gas := p.RequiredGas(input) if contract.UseGas(gas) {return p.Run(input)}return nil, ErrOutOfGas}

目前,Ethereuem 代码中已经加入了多个预编译合约,功能覆盖了包括椭圆曲线密钥恢复,SHA-3(256bits)哈希算法,RIPEMD-160 加密算法等等。相信基于自身业务的需求,二次开发者完全可以加入自己的预编译合约,大大加快合约的执行速度。

解释器执行合约的指令

解释器 Interpreter 用来执行(非预编译的)合约指令。它的结构体 UML 关系图如下所示:

? Interpreter 结构体通过一个 Config 类型的成员变量,间接持有一个包括 256 个operation 对象在内的数组 JumpTable。operation 是做什么的呢?

每个 operation 对象正对 应 一 个 已 定 义 的 虚 拟 机 指 令 , 它 所 含 有 的 四 个 函 数 变 量 execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。每个指令长度1byte,Contract 对象的成员变量 Code 类型为[]byte,就是这些虚拟机指令的任意集合,operation 对象的函数操作,主要会用到 Stack,Memory, IntPool 这几个自定义的数据结构。

? 这样一来,Interpreter 的 Run()函数就很好理解了,其核心流程就是逐个 byte 遍历入参 Contract 对象的 Code 变量,将其解释为一个已知的 operation,然后依次调用该operation 对象的四个函数,流程示意图如下:

operation 在操作过程中,会需要几个数据结构: Stack,实现了标准容器 -栈的行为;Memory,一个字节数组,可表示线性排列的任意数据;还有一个 intPool,提供对big.Int 数据的存储和读取。

已定义的 operation,种类很丰富,包括:

? 算术运算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP…;? 逻辑运算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT…;? 业务功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2…等等需要特别注意的是 LOGn 指令操作,它用来创建 n 个 Log 对象,这里 n 最大是 4。还记得 Log 在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个 Receipt 对象用来记录这个交易的执行结果。Receipt 携带一个 尚力财经小编2022Log 数组,用来记录 tx 操作过程中的所有变动细节,而这些 Log,正是通过合适的 LOGn 指令-即合约指令数组(Contract.Code) 中的单个 byte,在其对应的 operation 里被创建出来的。每个新创建的 Log 对象被缓存在StateDB 中的相对应的 stateObject 里,待需要时从 StateDB 中读取。

标签: 成结构

抱歉,评论功能暂时关闭!

微信号已复制,请打开微信添加咨询详情!