区块链中文网

区块链交易与智能合约的执行

5526
发表时间:2019-01-28 12:53来源:TRIAS

  Trias联合“北大软微-八分量协同创新实验室”定期举办技术沙龙。该实验室成立于2017年9月份,以可信计算、区块链等作为主要研究方向,致力于推动智能互联新时代下的人机互信问题的解决。

  现在,我们会推出由实验室教授、博士生以及硕士生主笔撰写的系列文章。本期文章由北京大学的博士生王与琛撰写。

  1.智能合约的源起

  区块链技术自比特币诞生之日起便受到了广泛的关注。最初,区块链仅仅作为记录用户交易的底层账本,不支持用户订制其它功能。

  比特币为了实现交易,即用户间转账的功能,设计了一套基于栈的简单脚本语言。这套语言不支持循环,不具备图灵完备性,仅限于比特币客户端内部使用,且只围绕交易这一项功能,一般被称之为“Bitcoin Script”。

  如果大家去查看比特币区块链中的每一笔交易记录,那么会发现交易内容其实是一串字节码,这串字节码就是Bitcoin Script。比特币对Bitcoin Script书写的交易代码的格式进行了限制,这种做法保证了交易的合法性与资金的安全性,但牺牲了整个系统的可编程性与灵活性。

  一般情况下,一套语言能实现成千上万种功能,如果设计一套语言只是实现了一个功能,未免有些可惜。或许Vitalik Buterin正是发现了区块链中脚本语言的可能性,于是他在以太坊中把语言请到了舞台中央,供用户创建与调用,这也成为了以太坊最具魅力的特性——智能合约。

  与比特币的Bitcoin Script对应,以坊中脚本语言名字叫EVM语言(Ethereum Virtual Machine Code)。EVM语言也是基于栈的语言,但它是图灵完备的语言,且以太坊设计了专门的虚拟机EVM来为其提供运行环境,这和Bitcoin Script有明显的区别。

  2.智能合约的使用以交易为接口

  为了明确系统的功能,以太坊扩充了交易的概念。在比特币中,交易一般指用户之间的转账操作,在以太坊中,交易除了转账,还包括创建或者调用智能合约。因此可以说EVM语言也是为了交易而存在,但它服务的交易的内容广泛得多。

  我们先补充一些必要的概念。以太坊中账户分为外部账户与合约账户,外部账户就是用户使用的账户,其中包括了用户的私钥和钱包等重要信息;合约账户用来存放一个智能合约,通常是由外部账户创建的。

  用户发送到以太坊区块链上的每一笔交易中都包含几个关键字段:“from”表示交易发起者,“to”表示交易接收者,“value”表示交易金额,“data”表示附带的信息。

  上文提及的三种操作的交易格式如下:

  1) 普通转账操作:“from A, to B, value C”表示从外部账户A向外部账户B转账,转账金额为C;

  2) 智能合约创建操作:“from A, to (空), value C, data D”表示外部账户A创建一个智能合约,向该合约账户里转账C, 合约的代码为D;

  3) 智能合约调用操作:“from A, to E, data F”表示外部账户A调用合约账户E的智能合约,本次调用传入的参数为F。

  3一笔交易的处理流程

  下面我们来分析一笔交易在以太坊区块链中是如何被处理与执行的。

  这部分在以太坊的源码中十分清晰,因此我们跟随源码里的函数调用流程来进行说明。以太坊Go版本源码地址:https://github.com/ethereum/go-ethereum。

  首先先定位,我们可以从core/blockchain.go中找到执行的core/state_processor.go中Process()方法,在Process()方法中可以找到如下一行代码:

  receipt, _,err:= ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, usedGas, cfg)

  根据这个函数名字我们知道已经找到了执行交易的入口。

  3.1 创建EVM虚拟机

  浏览在core/state_processor.go中的ApplyTransaction()方法,可发现如下三行关键代码:

  context := NewEVMContext(msg, header, bc, author)

  vmenv := vm.NewEVM(context, statedb, config, cfg)

  _, gas, failed, err := ApplyMessage(vmenv, msg, gp)

  第一行是创建新的EVM的执行上下文环境,第二行是创建新的EVM,第三行是用新创建的EVM来处理交易消息。由此可知,每一笔交易在执行之前以太坊都会创建一个EVM虚拟机来负责该交易的执行。

  继续浏览core/state_transition.go中的ApplyMessage()方法,发现该方法只有一行代码:

  return NewStateTransition(evm, msg, gp).TransitionDb()

  继续看core/state_transition.go中的TransitionDb()方法,发现方法中有一个重要的分支:

  if contractCreation {

  ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)

  } else {

  st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)

  ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)

  }

  这段代码的意思是先判断是不是创建合约的操作,如果是,则调用Create()方法,如果不是,则调用Call()方法。由此可知,交易的三种操作中,创建合约使用的方法是Create(),而调用合约与转账使用的方法是Call()。

  接下来我们分别看一下这两个方法。

  3.2 智能合约的创建

  先看core/vm/evm.go中的Create()方法。该方法只有两行代码:

  contractAddr=crypto.CreateAddress(caller.Address(),evm.StateDB.GetNonce(caller.Address()))

  return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)

  第一行是根据外部账户的地址和nonce值计算将要创建的合约账户的地址,这个nonce参数用来记录该外部账户已创建的合约的数目。

  第二行是将创建的合约账户地址和其它参数一起传给同文件中的create()方法。

  继续看create()方法可看到如下三行代码:

  nonce := evm.StateDB.GetNonce(caller.Address())

  evm.StateDB.SetNonce(caller.Address(), nonce+1)

  contractHash := evm.StateDB.GetCodeHash(contractAddr)

  第一行是获取想创建合约的外部账户的nonce值。

  第二行是将该nonce的值加一后写回去,第三行是计算合约地址的哈希值确保不会发生地址冲突。

  在计算出合约地址后,可看到下面一行代码:

  evm.StateDB.CreateAccount(contractAddr)

  这行代码是根据合约地址创建出了合约账户。合约账户创建完后,可看到下面一行代码:

  evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)

  创建者从外部账户向合约账户转账,金额为value。至此,合约账户创建工作完成了。

  接下来需要创建合约对象并把合约代码跑起来:

  contract:=NewContract(caller, AccountRef(contractAddr), value, gas) contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)

  第一行是创建合约对象,第二行是将用户定义的智能合约代码绑定到该合约对象上。

  合约对象创建完后,用下面一行代码运行该合约:

  ret, err = run(evm, contract, nil)

  可能有人会疑惑:创建完合约为什么要运行一遍?

  这主要有两方面的原因:其一,系统需保证合约代码是能正确运行的,这样在以后的调用中才不会出错;其二,系统需要通过运行才能计算出消耗的gas数量,进而完成对外部账户的创建合约操作的扣费。其实在create()方法中还有检查栈深度、创建快照、出错后回滚、gas计算等代码,因它们不涉及到本文的主要内容,故略过。

  3.3 转账与智能合约的调用

  然后我们看core/vm/evm.go中的Call()方法,该方法负责合约调用和转账两种交易操作。

  Call()方法中不需要创建新的地址,只需要:

  to = AccountRef(addr)

  该行代码获取交易接收方的地址。之后可看到下面一行代码:

  evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

  交易发起者向交易接收方转账value金额。接下来的代码和Create()相像:

  contract := NewContract(caller, to, value, gas)

  contract.SetCallCode(&addr,evm.StateDB.GetCodeHash(addr),evm.StateDB.GetCode(addr))

  第一行创建合约对象,第二行将接收者地址上的智能合约代码绑定到合约对象上。如果是转账操作,接收者地址上没有代码,因此绑定的代码是空,之后的运行会很快结束。绑定完成后,使用run()方法运行该合约完成调用:

  ret, err = run(evm, contract, input)

  在Call()方法中同样还有检查栈深度、创建快照、出错会滚、gas计算等代码,留给感兴趣的读者自行阅读。

  Create()与Call()中最后执行都调用了core/vm/evm.go中的run()方法,而run()方法中可发现:

  return interpreter.Run(contract, input, readOnly)

  接下来定位到core/vm/interpreter.go中的Run()方法。该方法是EVM中解释器的运行流程,核心逻辑为循环取出合约的代码,查表解析出具体的操作码,再查表计算出需要消耗的gas数目,然后调用操作码相应的处理函数执行。

  核心代码如下:

  for atomic.LoadInt32(&in.evm.abort) == 0 {

  …

  op = contract.GetOp(pc)

  operation := in.cfg.JumpTable[op]

  …

  cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)

  …

  res, err := operation.execute(&pc, in, contract, mem, stack)

  …

  }

  至此,一笔交易的运行就结束了。

  4. 结语

  综上所述,我们能了解到以太坊设计的三种交易背后能给予用户极大的自由度,也充分发挥了EVM语言及其虚拟机的功能。

  用户通过Solidity等语言编写智能合约,然后编译成EVM语言,再打包成交易的格式发送到区块链上运行。以太坊得益于这种模式带来的可编程的特性,引领区块链技术进入了2.0时代。

  然而,现在智能合约由于EVM的栈深度与gas消耗的限制,多数都是简单且袖珍的程序。即使现在这样的程序已经足够满足需求,但未来必将面临更多更加复杂化的交易场景。

  如何去应对这些场景,需要广大开发者们继续努力。


分享到: