Hyperledger Burrow EVM

Hyperledger Burrow由Linux基金会托管,最初由Monax设计,Monax是一个开放平台,可为企业生态系统构建,交付和运行基于区块链的应用程序。著名的处理器和芯片制造商英特尔也共同赞助了该项目。

Hyperledger Burrow充当许可的智能合约应用程序引擎,其主要工作是以安全有效的方式执行和处理智能合约程序。它是为支持特定于应用程序的优化的多链环境而构建的。

以太坊等许多区块链网络都支持智能合约,自行执行的合约,这些合约的合约条款直接写入代码中。简而言之,Hyperledger Burrow充当智能合同解释器,促进遵循以太坊虚拟机(EVM)标准的网络上此类合同的执行。EVM使用全球公共节点网络执行以太坊智能合约脚本。Burrow充当区块链上的一个节点,该区块链使用EVM标准来提供各种智能合约交易的结论性和高交易吞吐量。

Hyperledger Burrow包含以下组件:

共识引擎负责在区块链上订购和处理各种交易,并确保高交易量输出。它具有一组内置的事务验证器,还可以防止任何可能的恶意企图来破解和分叉区块链。共识引擎与智能合约应用程序无关,因为另一层应用程序区块链接口(ABCI)将两者分开,从而确保了核心引擎与各种应用程序(有时可能包括恶意应用程序)之间的安全性。

每当在区块链网络上发生的交易要求执行智能合约代码时,智能合约应用程序(SCA)组件都会在许可的EVM中激活该帐户代码的必要执行。EVM的工作是确保执行应用程序的代码遵守以太坊操作代码规范,并正确授予所需的权限。Burrow通过EVM模块支持合约的执行与调用,调用时根据合约地址获取到代码,生成环境后载入到EVM中运行。通常智能合约的开发流程是用solidlity编写逻辑代码,再通过编译器编译元数据,最后再发布。

虽然Burrow遵循以太坊虚拟机标准,但是其EVM的设计与以太坊的虚拟机还是有一定差别。

Burrow EVM

Hyperledger Burrow 的EVM代码存放于execution\evm中。
Hyperledger Burrow EVM_第1张图片
其结构:
Hyperledger Burrow EVM_第2张图片
Burrow EVM 其他部分与以太坊虚拟机差别不大,都是基于栈的虚拟机,其中:
•contract.go 定义了智能合约的数据结构
•evm.go 定义了执行器,以及对外提供一些外部接口
•memory.go 定义了EVM的内存
•stack.go 定义了EVM的栈

gas

其gas花费代码位于execution\native\gas.go中,与以太坊虚拟机不同,Burrow EVM每次花费固定为1:
Hyperledger Burrow EVM_第3张图片

智能合约

合约是EVM智能合约的存储单位也是解释器执行的基本单位,包含了代码,调用人,所有人,gas相关的信息。定义于native\contract.go中:

type Contract struct {
	// Comment describing purpose of native contract and reason for assembling
	// the particular functions
	Comment string
	// Name of the native contract
	Name          string
	functionsByID map[abi.FunctionID]*Function
	functions     []*Function
	address       crypto.Address
	logger        *logging.Logger
}

执行环境

执行入口定义在evm.go中,功能是组装执行环境(代码,执行人关系,参数等)。同样需要创建调用对象的state,但参数稍有不同,这里没有考虑别人合约可能需要花钱。

func (vm *EVM) Execute(st acmstate.ReaderWriter, blockchain engine.Blockchain, eventSink exec.EventSink,
	params engine.CallParams, code []byte) ([]byte, error) {

	// Make it appear as if natives are stored in state
	st = native.NewState(vm.options.Natives, st)

	state := engine.State{
		CallFrame:  engine.NewCallFrame(st).WithMaxCallStackDepth(vm.options.CallStackMaxDepth),
		Blockchain: blockchain,
		EventSink:  eventSink,
	}

	output, err := vm.Contract(code).Call(state, params)
	if err == nil {
		// Only sync back when there was no exception
		err = state.CallFrame.Sync()
	}
	// Always return output - we may have a reverted exception for which the return is meaningful
	return output, err
}

ABI

ABI即Application Binary Interface,是应用程序二进制接口,描述了应用程序和EVM之间,一个应用和它的库之间,或者应用的组成部分之间的低接口。ABI涵盖了各种细节,如:
●数据类型的大小、布局和对齐;
●调用约定(控制着函数的参数如何传送以及如何接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后;
●系统调用的编码和一个应用如何向EVM进行系统调用;
●以及在一个完整的EVM的ABI中,目标文件的二进制格式、程序库等等。

这么说可能有点难懂,先从智能合约的角度说起。

我们编写智能合约的流程是:
●编写合约代码(一般使用solidity语言)
●编译合约,将solidity编写的代码编译成EVM可识别的bytecode,这一步生成abi
●部署合约,将合约部署到区块链上,生成合约地址,将合约内容(即上一步生成的bytecode)作为input date输入。部署合约是一个交易过程,所以也会生成一个交易Hash
●执行合约,获取合约地址,然后传入参数调用合约中的方法,获得执行结果

从上面的步骤可以看出,abi对于EVM来说,其实是不需要的。但是对于调用者来说,就需要知道合约有哪些方法,方法的参数是什么,返回值是什么,而这些信息就记录在智能合约的abi中。
所以简单来说,ABI其实就相当于开发者的接口文档,方便开发者调用执行合约。

以一个简单的solidity为例,通过Web3,Remix等工具编译生成ABI。

pragma solidity ^0.4.24;


contract Demo {

    uint private x;

    function set(uint _x) public {
        x = _x;
    }

}

执行 truffle compile 编译合约,就会生成对应的文件Demo.json

{
  "contractName": "Demo",
  "abi": [
    {
      "constant": false,
      "inputs": [
        {
          "name": "_x",
          "type": "uint256"
        }
      ],
      "name": "set",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],
  "bytecode": "0x6080604052348015600f57600080fd5b5060a48061001e6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b1146044575b600080fd5b348015604f57600080fd5b50606c60048036038101908080359060200190929190505050606e565b005b80600081905550505600a165627a7a723058201dfe7c019fec67ccd87250c9ac8642c163cc5f43588715b33e8a8953df3715f60029",
  "deployedBytecode": "0x608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b1146044575b600080fd5b348015604f57600080fd5b50606c60048036038101908080359060200190929190505050606e565b005b80600081905550505600a165627a7a723058201dfe7c019fec67ccd87250c9ac8642c163cc5f43588715b33e8a8953df3715f60029",
  "sourceMap": "27:97:1:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;27:97:1;;;;;;;",
  "deployedSourceMap": "27:97:1:-;;;;;;;;;;;;;;;;;;;;;;;;69:52;;8:9:-1;5:2;;;30:1;27;20:12;5:2;69:52:1;;;;;;;;;;;;;;;;;;;;;;;;;;;112:2;108:1;:6;;;;69:52;:::o",
  "source": "pragma solidity ^0.4.24;\n\n\ncontract Demo {\n\n    uint private x;\n\n    function set(uint _x) public {\n        x = _x;\n    }\n\n}\n",
  "sourcePath": "/Users/root/Workspace/DApp/demo/contracts/Demo.sol",
  "ast": {
    ...
  },
  "legacyAST": {
    ...
  },
  "compiler": {
    "name": "solc",
    "version": "0.4.24+commit.e67f0147.Emscripten.clang"
  },
  "networks": {},
  "schemaVersion": "2.0.1",
  "updatedAt": "2018-09-14T11:57:49.750Z"
}

其中,各字段的意义分别为:
name:函数名称
type:方法类型,包括function, constructor, fallback(缺省方法)可以缺省,默认为function
constant:布尔值,如果为true指明方法不会修改合约字段的状态变量
payable:布尔值,标明方法是否可以接收ether
stateMutability:状态类型,包括pure (不读取区块链状态),view (和constant类型,只能查看,不会修改合约字段),nonpayable(和payable含义一样),payable(和payable含义一样)。其实保留payable和constant是为了向后兼容
inputs:数组,描述参数的名称和类型
name:参数名称
type:参数类型
outputs:和inputs一样,如果没有返回值,缺省是一个空数组

在使用ABI调用合约函数时,传入的ABI会被编码成calldata(一串hash值)。calldata由function signature和argument encoding两部分组成。通过读取call data的内容, EVM可以得知需要执行的函数,以及函数的传入值,并作出相应的操作。

EVM读取并执行call data的规则:
●函数选择器: Call data的前4个bytes对应了合约中的某个函数。因此EVM通过这4个bytes, 可以跳转到相应的函数。

ByteCode中的体现:
Hyperledger Burrow EVM_第4张图片
Hyperledger Burrow EVM_第5张图片
上面两张图是合约bytecode最开始的部分,EVM依次执行每条命令,当执行到CALLDATASIZE(L6)时,EVM读取input的size(读取的是function的hash, 长度为4bytes)。L7会和4进行比较,作为L9的JUMPI的跳转条件

input的size比4小,则跳转到fallback function(fallback function是唯一一个没有名字的函数)。跳转的地址是由L8 push到栈中的,跳转的地址是6d,转换成10进制为109,也就是L42。

input的size长度为4,则会继续执行每个指令。L11的CALLDATALOAD会从读取input的具体值,与L18,23,28,33,38的hash值(该合约中的函数签名)进行比较,以L18为例,L19会比较input的函数签名与L18的哈希值是否相等。如果相等,则跳转到L20表明的地址(PC:8d),否则继续执行,直到遇到函数签名相等的情况为止

如果没有一个函数签名相等(此时执行到L41),因为没有执行到终止命令,如stop。EVM会继续往下执行L42,L42是fallback function的起始位置。因此fallback function的执行情况是:
A contractcan have exactly one unnamed function. This function cannot have arguments and cannot return anything. It is executed on a call to the contract if none of theother functions match the given function identifier. Furthermore, this functionis executed whenever the contract receives plain Ether (without data).

●参数读取:call data是32bytes的整数倍(头4 bytes的函数签名除外),EVM通过CALLDATALOAD指令,每次能从call data中读取32byte的值,放入stack中。上面的图通过2个CALLDATALOAD分别读出了newState和value的值,放入了stack中。通过JUMP指令跳转到函数体(tag19),并继续执行。

●返回值:

当函数结束完时,会跳转到tag18。Tag 18最末尾的RETURN指令。RETURN指令会从stack中读取两个值,通过这两个值从memory中读取相应的值,以返回给调用方。

ABI是如何编码的

●函数编码:在EVM中,每个函数都由4个byte长度的16进制值来唯一标识,这4个bytes叫做函数签名。函数签名是对函数名,函数参数做Keccak(SHA-3) 运算后,获得的hash值的前4个bytes.

如下面increaseAge这个函数,直接通过web3提供的sha3,取前4bytes即可获得函数签名F9EA5E79. EVM会通过这个函数签名找到对应的函数,通过JUMPI跳转到对应的函数。

functionincreaseAge(string name, uint num)returns (uint){

       return ++age;
}

●函数参数编码:

每个参数都是以一个32byte长度hash值的形式传入的,长度不够用0补,如uint8的长度是8bytes,前面不足的24bytes都用0来补。用下面的合约来举例:

pragma solidity ^0.4.16;

contract Foo {
  function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
  function bar(bytes3[2] memory) public pure {}
  function f(uint, uint32[], bytes10, bytes) public pure {}
}

案例一:
函数:baz(bytes3[2] memory)
调用:baz(69, true)

0xcdcd77c0,在node中使用new Web3().sha3('baz(uint32,bool)')生成

const Web3 = require('web3')

const web3 = new Web3()
console.log(web3.sha3('f(uint256,uint32[],bytes10,bytes)'))

0x0000000000000000000000000000000000000000000000000000000000000045,十进制69,转成16进制为45,因为是正数,高位补0至32bytes

0x0000000000000000000000000000000000000000000000000000000000000001,bool类型,true=1,false=0,高位补0
所以最终字符串为:

0xcdcd77c0
0000000000000000000000000000000000000000000000000000000000000045
0000000000000000000000000000000000000000000000000000000000000001

一共68bytes。

返回:该函数返回的是true,output将会是

0x0000000000000000000000000000000000000000000000000000000000000000
案例二:
函数:bar(bytes3[2] memory)
调用:bar(["abc", "def"])

0xfce353f6,在node中使用new Web3().sha3('bar(bytes3[2])')生成
固定长度不需要计算偏移量
0x6162630000000000000000000000000000000000000000000000000000000000,字符串abc转成16进制后为616263,低位补0
0x6465660000000000000000000000000000000000000000000000000000000000,同上
所以最终字符串为:

0xfce353f6
6162630000000000000000000000000000000000000000000000000000000000
6465660000000000000000000000000000000000000000000000000000000000

案例三:
函数:f(uint,uint32[],bytes10,bytes)
调用:

f(0x123, [0x456, 0x789], "1234567890", "Hello, world!")

0x8be65246,在node中使用f(uint256,uint32[],bytes10,bytes)生成
0x00000000000000000000000000000000000000000000000000000000000001230x123对应的16进制,正数补全
0x0000000000000000000000000000000000000000000000000000000000000080,动态类型,计算偏移量。这个的偏移量是指实际存储值的位置,由于这个函数有4个变量,那么实际存储值的位置就是第五个32bytes位置,也就是说偏移量等于4×32bytes=128,转成16进制后就是对应的值
0x3132333435363738393000000000000000000000000000000000000000000000,字符串1234567890转成16进制后为31323334353637383930,bytes类型,低位补全
0x00000000000000000000000000000000000000000000000000000000000000e0,动态类型,计算偏移量,这个偏移量就等于参数长度4×32bytes+前面的动态参数参数占有的长度(因为前面只有一个动态参数,所以这个长度就是1×32bytes+2×32bytes,1×32bytes是第一个动态参数长度所占的bytes数,2×32bytes是因为该函数中的第一个动态参数有2个值),那么具体的值就是 4×32bytes+(1×32bytes+2×32bytes)=7×32bytes=224,转成16进制就是e0,高位补全就是对应的值
0x0000000000000000000000000000000000000000000000000000000000000002,第一个动态参数的长度,长度为2
0x0000000000000000000000000000000000000000000000000000000000000456,第一个动态参数中的第一个元素
0x0000000000000000000000000000000000000000000000000000000000000789,第一个动态参数中的第二个元素
0x000000000000000000000000000000000000000000000000000000000000000d,第二个动态参数的长度,长度为13
0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000,第二个动态参数的值编码
所以最终字符串为:

0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000

用ABI与smart contract进行交互

当DAPP端调用smart contract的某个函数时,web3的作用就是把ABI通过网络发送给Node。Node接收到ABI之后,编译成hash值并且执行。Node把执行完的结果上传到区块链。如果有返回值,Node再通过网络的方式返回给DAPP。

你可能感兴趣的:(Hyperledger Burrow EVM)