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

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消耗的限制,多数都是简单且袖珍的程序。即使现在这样的程序已经足够满足需求,但未来必将面临更多更加复杂化的交易场景。
如何去应对这些场景,需要广大开发者们继续努力。
撰文 | 王与琛
编辑 | 郑辰
出品 | Trias团队