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中。
其结构:
Burrow EVM 其他部分与以太坊虚拟机差别不大,都是基于栈的虚拟机,其中:
•contract.go 定义了智能合约的数据结构
•evm.go 定义了执行器,以及对外提供一些外部接口
•memory.go 定义了EVM的内存
•stack.go 定义了EVM的栈
gas
其gas花费代码位于execution\native\gas.go中,与以太坊虚拟机不同,Burrow EVM每次花费固定为1:
智能合约
合约是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中的体现:
上面两张图是合约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)
生成
0x0000000000000000000000000000000000000000000000000000000000000123
,0x123
对应的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。