深入了解以太坊虚拟机第5部分——一个新合约被创建后会发生什么

在该系列文章的前部分,我们学了EVM汇编基础,也学了ABI编码是如何允许外部程序与合约进行通信的。在本文中,我们将会学习一个合约是如何从零创建的。

本系列的相关文章(按照顺序):

我们目前所见的EVM字节码都是比较清晰明朗的,就是EVM从上往下的执行指令,没有什么隐藏的魔法。合约创建的过程更有意思一些,它将数据和代码之间的界限模糊化。

在学习合约是如何创建的时候,我们将会看到有时候数据就是代码,有时候代码就是数据。

带上你最喜欢的魔术帽子??,我们来开始吧!

合约出生证明

让我们创建一个简单(完全没用)的合约:

pragma solidity ^0.4.11;
contract C {
}

编译它:

solc --bin --asm c.sol

字节码是:

60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

为了创建这个合约,我们需要先通过发送一个eth_sendtransaction RPC请求给以太坊的节点来创建一个交易。你可以使用RemixMetamask来做这件事情。

不管你使用什么开发工具,RPC调用的参数就差不多类似于:

{
  "from": "0xbd04d16f09506e80d1fd1fd8d0c79afa49bd9976",
  "to": null,
  "gas": "68653", // 30400,
  "gasPrice": "1", // 10000000000000
  "data": "0x60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058204bf1accefb2526a5077bcdfeaeb8020162814272245a9741cc2fddd89191af1c0029"
}

没有什么特殊的RPC调用或交易类型来创建一个合约。相同的交易机制也被用于其他机制:

  • 转移Ether到一个账户或合约
  • 调用一个带参数的合约方法

根据你指定的参数,以太坊会以不同方式解释交易。创建一个合约,to地址应该为null(或被忽略)。

我用下面这个交易创建了一个合约例子:

https://rinkeby.etherscan.io/tx/0x58f36e779950a23591aaad9e4c3c3ac105547f942f221471bf6ffce1d40f8401

打开Etherscan,你应该可以看到该交易的输入数据就是Solidity编译器产生的字节码:

当处理该交易的时候,EVM会将输入数据作为代码执行。瞧,一个合约就被创建了。

字节码是干什么的?

我们可以将上面的字节码分成3个独立的块:

//部署代码
60606040523415600e57600080fd5b5b603680601c6000396000f300
//合约代码
60606040525b600080fd00
// Auxdata
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
  • 创建合约时运行部署代码
  • 合约创建成功之后当它的方法被调用时,运行合约代码
  • (可选)Auxdata是源码的加密指纹,用来验证。这只是数据,永远不会被EVM执行

部署代码有两个主要作用:

  1. 运行构造器函数,并设置初始化内存变量(就像合约的拥有者)
  2. 计算合约代码,并返回给EVM

Solidity编译器产生的部署代码会从字节码中加载60606040525b600080fd00到内存中,然后将它作为合约代码返回。在这个例子中,“计算”只是读取一块数据到内存中。原则上,我们可以编程地产生合约代码。

构造器到底做什么取决于语言,但是EVM语言最后会返回合约代码。

合约创建

那么当部署代码运行完并返回合约代码之后会发生什么?以太坊是如何从返回的合约代码中创建一个合约的?

让我们一起深入的去了解一下源码,看看细节。

我发现了Go-Ethereum 的实现方式是找到需要信息的最简单参考。我们得到了正确的变量名、静态类型信息和符号交叉引用。尝试超越这个,黄皮书!

使用Sourcegraph(当鼠标停留在一个变量上的时候会有类型信息,非常好用)软件阅读的源码,找到的相关方法是evm.Create。让我们略读一下代码,忽略一些错误检查和过于详细的细节。从上到下:

  • 检测调用者是否拥有足够的余额来做转账
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
 return nil, common.Address{}, gas, ErrInsufficientBalance
}
  • 从调用者的地址派生一个新合约的地址(通过创建者账户的 nonce传递):
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
  • 使用派生的合约地址来创建新合约账户(改变”世界状态“StateDB ):
evm.StateDB.CreateAccount(contractAddr)
  • 将初始的Ether基金从调用者转到新合约中:
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
  • 设置输入数据为合约的部署代码,然后使用EVM来执行。ret变量是返回的合约代码:
contract := NewContract(caller, AccountRef(contractAddr), value, gas)
contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
ret, err = run(evm, snapshot, contract, nil)
  • 检查错误。或如果合约代码太长则会失败。收取用户的gas然后设置合约代码:
if err == nil && !maxCodeSizeExceeded {
  createDataGas := uint64(len(ret)) * params.CreateDataGas
  if contract.UseGas(createDataGas) {
    evm.StateDB.SetCode(contractAddr, ret)
  } else {
    err = ErrCodeStoreOutOfGas
  }
}

部署代码的代码

让我们来看看汇编代码的细节,看看当一个合约被创建的时候”部署代码“是如何返回”合约代码“的。我们将会再一次分析合约列子:

pragma solidity ^0.4.11;
contract C {
}

将该合约的字节码分成独立的块:

// 部署代码
60606040523415600e57600080fd5b5b603680601c6000396000f300
//合约代码
60606040525b600080fd00
// Auxdata
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

部署代码的汇编代码是:

// 为Solidity内部保留0x60个字节的内存
mstore(0x40, 0x60)
// 非支付合约。如果调用者发送ether就会归还
jumpi(tag_1, iszero(callvalue))
0x0
dup1
revert
// 将合约代码拷贝到内存中并返回
tag_1:
tag_2:
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x0
  codecopy
  0x0
  return
stop

为返回合约代码跟踪上面的汇编代码:

// 60 36 (PUSH 0x36)
dataSize(sub_0)
  stack: [0x36]
dup1
  stack: [0x36 0x36]
// 60 1c == (PUSH 0x1c)
dataOffset(sub_0)
  stack: [0x1c 0x36 0x36]
0x0
  stack: [0x0 0x1c 0x36 0x36]
codecopy
  // 消耗三个参数
  // 将数据的 `length` 从`codeOffset` 拷贝到`memoryOffset`
  // memoryOffset = 0x0
  // codeOffset   = 0x1c
  // length       = 0x36
  stack: [0x36]
0x0
  stack: [0x0 0x36]
  memory: [
    0x0:0x36 => calldata[0x1c:0x36]
  ]
return
  // 消耗两个参数
  // 返回 `memoryOffset`中的数据`length`
  // memoryOffset  = 0x0
  // length        = 0x36
  stack: []
  memory: [
    0x0:0x36 => calldata[0x1c:0x36]
  ]

dataSize(sub_0)dataOffset(sub_0)实际上不是真正的指令。它们实际上是PUSH指令,将常量压入栈中。两个0x1C(28) 和0x36 (54) 常量指定一个字节码子字符串作为代码合约返回。

部署代码的汇编代码大致对应于下面的Python3 代码:

memory = []
calldata = bytes.fromhex("60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a72305820b5090d937cf89f134d30e54dba87af4247461dd3390acf19d4010d61bfdd983a0029")
size = 0x36   // dataSize(sub_0)
offset = 0x1c // dataOffset(sub_0)
// 将调用数据的子字符串拷贝到内存
memory[0:size] = calldata[offset:offset+size]
// 将内存的内容用十六进制打印出来而不返回
print(bytes(memory[0:size]).hex())

产生的内存内容是:

60606040525b600080fd00
a165627a7a72305820b5090d937cf89f134d30e54dba87af4247461dd3390acf19d4010d61bfdd983a0029

对应的汇编代码(加上auxdata):

// 6060604052600080fd00
mstore(0x40, 0x60)
tag_1:
  0x0
  dup1
  revert
auxdata: 0xa165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

再次看下Etherscan,这正是部署的合约代码:

以太坊账户0x2c7f561F1fc5c414C48d01E480fDAAE2840B8AA2 信息

以太坊区块链探险者,API和分析平台

rinkeby.etherscan.io

CODECOPY

部署代码使用codecopy将交易的输入数据拷贝到内存。

codecopy指令的行为和参数比其他的简单指令要复杂一点。如果我在黄皮书中查找这个指令,可能会更加的困惑一些。相反,让我们看看go-ethereum 源代码来研究一下到底怎么回事。

看看CODECOPY

func opCodeCopy(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
 var (
  memOffset  = stack.pop()
  codeOffset = stack.pop()
  length     = stack.pop()
 )
 codeCopy := getDataBig(contract.Code, codeOffset, length)
 memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
evm.interpreter.intPool.put(memOffset, codeOffset, length)
 return nil, nil
}

没有难懂的字母!

evm.interpreter.intPool.put(memOffset, codeOffset, length)这一行回收对象(大整数)后面使用。这只是一个高效的优化。

构造器参数

除了产生合约代码,部署代码的其他作用是运行构造器来进行设置。如果存在构造器参数,那么部署代码就需要从某地放加载参数。

Solidity传递构造器参数的惯例是在调用eth_sendtransaction时在字节码末尾附加ABI编码的参数值。RPC调用将字节码和ABI编码参数放到一起作为输入数据进行传递,就像:

{
  "from": "0xbd04d16f09506e80d1fd1fd8d0c79afa49bd9976"
  "data": hexencode(compiledByteCode + encodedParams),
}

看看有一个构造器参数的合约例子:

pragma solidity ^0.4.11;
contract C {
  uint256 a;
  function C(uint256 _a) {
    a = _a;
  }
}

我已经创建了这个合约,传递了66值。Etherscan上的交易:

https://rinkeby.etherscan.io/tx/0x2f409d2e186883bd3319a8291a345ddbc1c0090f0d2e182a32c9e54b5e3fdbd8

输入数据是:

0x60606040523415600e57600080fd5b6040516020806073833981016040528080519060200190919050508060008190555050603580603e6000396000f3006060604052600080fd00a165627a7a7230582062a4d50871818ee0922255f5848ba4c7e4edc9b13c555984b91e7447d3bb0e7400290000000000000000000000000000000000000000000000000000000000000042

我们可以在最后面看到构造器的参数,也就是66,但是作为ABI编码的32位字节就是:

0000000000000000000000000000000000000000000000000000000000000042

为了处理构造器里面的参数,部署代码从calldata中的结尾拷贝ABI参数到内存中,然后从内存中拷贝到栈中。

创建合约的合约

FooFactory合约可以通过调用makeNewFoo来创建一个新的Foo实例:

pragma solidity ^0.4.11;
contract Foo {
}
contract FooFactory {
  address fooInstance;
  function makeNewFoo() {
    fooInstance = new Foo();
  }
}

这个合约完整的汇编代码在Gist里。编译器输出的结构更加的复杂,因为这里有两套”安装时间“和”运行时间“的字节码。就像这样组织的:

FooFactoryDeployCode
FooFactoryContractCode
  FooDeployCode
  FooContractCode
  FooAUXData
FooFactoryAUXData

FooFactoryContractCode本质就是为tag_8Foo拷贝字节码然后跳转回tag_7去执行create指令。

create指令就像eth_sendtransactionRPC调用。它提供了一个在EVM里面创建新合约的方法。

看一下go-ethereum 源代码里面的opCreate 。这个指令调用evm.Create来创建合约:

res, addr, returnGas, suberr := evm.Create(contract, input, gas, value)

我们在前面就已经看见了evm.Create,但是这次的调用者是一个智能合约而不是人类。

AUXDATA

如果你想要完全的理解auxdata是什么,那么可以阅读 合约元数据。它的要点就是auxdata是一个哈希值,你可以使用它来抓取部署合约的元数据。

auxdata的格式就是:

0xa1 0x65 ‘b‘ ‘z‘ ‘z‘ ‘r‘ ‘0‘ 0x58 0x20 <32 bytes swarm hash> 0x00 0x29`

解构我们之前看到过的auxdata字节序列:

a1 65
// b z z r 0 (ASCII)
62 7a 7a 72 30
58 20
// 32 bytes hash
62a4d50871818ee0922255f5848ba4c7e4edc9b13c555984b91e7447d3bb0e74
00 29

总结

合约创建的方式和自我解压软件安装程序的工作方式比较类似。当安装程序运行时,它会配置系统环境,然后会从程序包中读取目标程序放入到系统中。

  • 在”安装时间“和”运行时间“之间有一个强制的分离。无法运行构造器两次
  • 智能合约可以使用相同的处理来创建其他的智能合约
  • 使用非Solidity语言实现会容易一点

首先,我对”智能合约安装程序“的不同部分被打包到一起作为字节字符串data放在交易里感到很困惑:

{
  "data": constructorCode + contractCode + auxdata + constructorData
}

data是如何被编码的,阅读文档中的eth_sendtransaction无法获得明显的答案。我一直都没弄明白构造器的参数是如何传递给交易的,直到有一个朋友告诉我它们被ABI进行编码之后附加到字节码的后面,才明白是怎么回事。

另一个可以使它看起来更加清晰一点的替代设计也许就是将这些作为交易独立的属性进行发送:

{
  // For "install time" bytecode
  "constructorCode": ...,
  // For "run time" bytecode
  "constructorBody": ...,
  // For encoding arguments
  "data": ...,
}

不过进行更多的思考,我认为交易对象简单化实际上是非常强大的。对于一个交易,data只是一个字节字符串,而且它不涉及数据是如何被解释的语言模型。通过让交易对象简单化,语言的实现者就有一个空白的画布进行设计和实验。

确实,data在未来甚至可以被一个不同的虚拟机进行解释。

本系列文章其他部分译文链接:

翻译作者: 许莉

原文地址:Diving Into The Ethereum VM Part Five

原文地址:https://www.cnblogs.com/405845829qq/p/9962740.html

时间: 2024-11-08 17:19:49

深入了解以太坊虚拟机第5部分——一个新合约被创建后会发生什么的相关文章

深入了解以太坊虚拟机第4部分——ABI编码外部方法调用的方式

在本系列的上一篇文章中我们看到了Solidity是如何在EVM存储器中表示复杂数据结构的.但是如果无法交互,数据就是没有意义的.智能合约就是数据和外界的中间体. 在这篇文章中我们将会看到Solidity和EVM可以让外部程序来调用合约的方法并改变它的状态. "外部程序"不限于DApp/JavaScript.任何可以使用HTTP RPC与以太坊节点通信的程序,都可以通过创建一个交易与部署在区块链上的任何合约进行交互. 创建一个交易就像发送一个HTTP请求.Web的服务器会接收你的HTTP

以太坊虚拟机介绍4-按位运算指令

以太坊虚拟机按位运算指令 EVM定义了8条按位运算指令,见下表: 下面是按位运算指令的操作码分布图: AND.OR.XOR.NOT AND.OR.XOR指令从栈顶弹出两个元素,进行按位运算,然后把结果推入栈顶.以AND指令为例,下面是它的操作示意图: NOT指令将栈元素按位取反,下面是它的操作示意图: 这四条指令分别与Solidity语言里的&.|.^和~运算符直接对应,下面的Solidity代码演示了这四条指令的具体应用(读者可以运行solc --asm --opcodes bitwise_d

以太坊虚拟机介绍

近期打算写一些关于以太坊虚拟机(后面简称EVM)的文章,这是其中的第一篇.这一系列文章想站在EVM指令集的角度,带领读者逐步理解EVM工作原理,进而理解以太坊区块链技术细节.由于网上介绍以太坊的文章也比较多,所以这一系列文章将最大程度减少不必要的废话,直接提供文章想要表达的信息. EVM基本信息编程语言虚拟机一般有两种类型,基于栈,或者基于寄存器.大部分我们所熟知的语言都采用基于栈的虚拟机,比如最著名的Java虚拟机.在游戏领域非常流行的Lua语言则采用了基于寄存器的虚拟机.和JVM一样,EVM

以太坊虚拟机介绍5-比较操作指令

以太坊虚拟机比较操作指令 EVM定义了6条比较操作指令,见下表: 下面是比较操作指令的操作码分布图: LT.GT.SLT.SGT.EQ这5条指令都是从栈顶弹出两个元素,进行比较,然后把结果(1表示true,0表示false)推入栈顶.其中LT和GT把弹出的元素解释为无符号整数进行比较,SLT和SGT把弹出的元素解释为有符号数进行比较,EQ不关心符号.以LT指令为例,下面是它的操作示意图: ISZERO ISZERO指令从栈顶弹出一个元素,判断它是否为0,如果是,则把1推入栈顶,否则把0推入栈顶.

以太坊虚拟机介绍3-算术运算指令

以太坊虚拟机算术运算指令EVM总共定义了11条算术运算指令,见下表: 这些指令从栈顶弹出两到三个元素,进行相应计算,然后把结果推入栈顶.参与计算的元素和结果均被解释为按二的补码编码的整数.如果计算结果(假设为x)溢出(超出2^256),则最终的结果x'取值x % 2^256(%表示取模运算,^表示指数运算). 下面是算术运算指令的操作码分布图: ADD.MUL.SUB.DIV.SDIV.MOD.SMOD.EXP这8条指令操作方式比较类似,从栈顶弹出两个元素,进行计算,然后把计算结果推入栈顶.由于

谈一谈以太坊虚拟机EVM的缺陷与不足

首先,EVM的设计初衷是什么?它为什么被设计成目前我们看的样子呢?根据以太坊官方提供的设计原理说明,EVM的设计目标主要针对以下方面: 简单性(Simplicity) 确定性(Determinism) 节省空间的bytecode 专为区块链设计 更加简单的安全性保证 容易优化 如果读者浏览一下这个文档,会发现EVM的设计看上去都非常的合理.那么问题在哪里呢?问题就出在它和目前主流的技术以及设计范例都格格不入.EVM如果作为一个毫无限制的非现实世界中的设计确实很不错.接下来笔者会围绕EVM各个方面

以太坊去中心化淘宝智能合约案例

篇文章我们来介绍一个简易的区块链电商系统的核心功能,10多年来,我们习惯了淘宝的电商模式,淘宝为电商在中国普及做出了突出贡献,值得肯定,也完成了历史使命. 淘宝模式的核心是什么? 免费是一方面,我认为最核心的是解决了网上交易买卖家的信任问题,通过支付宝作为资金中介,使买卖家可以放心的交易. 但是,淘宝迅速崛起为巨头后,交易环节出现了很多不好的现象:刷信誉.巨大经济利益带来的内部腐败等等.因此,淘宝虽然成为了电商巨头,但是却没有真正的解决电商产业的信誉问题.这个问题其实不是淘宝特有,是一种社会现象

【以太坊开发】如何开发一个编译以太坊智能合约并且发布的平台(二)

接上一章的内容,这篇介绍 deploy相关和结果演示. deploy一个合约的过程中,需要计算发布的消耗和nonce值. 当进行每笔交易时,发送人设定Gas Limit 和Gas Price,将 Gas Limit*Gas Price ,就得到了ETH交易佣金的成本. nonce:以太坊要求一个账户的每笔交易有一个连续的计数.每个节点将根据计数顺序严格执行来自一个用户的交易. app.js中有下面两个函数: var web3 = new Web3(new Web3.providers.HttpP

以太坊虚拟机介绍2-栈操作指令

上一篇文章对EVM和它的指令集进行了简单介绍,本文将介绍POP指令.PUSHx系列指令.DUPx系列指令.SWAPx系列指令.这些指令只对EVM栈进行单纯的操作,它们的操作码分布如下图所示: POP指令POP指令(操作码0x50)从栈顶弹出一个元素.下面是POP指令的操作示意图(白色表示元素即将发生变动): PUSHx指令PUSH系列指令把紧跟在指令后面的N(1 - 32)字节元素推入栈顶.PUSH系列指令一共有32条,从PUSH1(操作码0x60)一直到PUSH32(操作码0x7A).EVM是