原标题:《以太坊智能合约ABI、事件和日志》
作者:北京大学胡悦阳,本文仅代表作者观点
原文链接:https://mp.weixin.qq.com/s/QKz8r1MpntGuw_xM9gh9_w
智能合约是什么
智能合约是在区块链上运行的应用或程序。通常情况下,它们为一组具有特定规则的数字化协议,且该协议能够被强制执行。这些规则由计算机源代码预先定义,网络节点会复制和执行这些计算机源码。
简单的说,智能合约就是区块链上一个包含合约代码和存储空间的虚拟账户。
智能合约的行为由合约代码控制,而智能合约的账户存储则保存了合约的状态。
在以太坊平台上,智能合约的代码运行在以太坊虚拟机中,EVM是一个图灵完备的虚拟机,是以太坊的核心。
在以太坊的点对点网络中,每个全节点上都包含一个以太坊虚拟机,当节点需要打包或验证区块时,便将交易相关的可执行代码送入EVM中执行,执行的结果更新了以太坊账户的状态并被记录在区块链上。
智能合约的操作
创建智能合约的流程:
编写智能合约的代码编译智能合约的代码变成可以在EVM上执行的bytecode,同时可以通过编译获得智能合约的ABI部署到区块链,通过一个交易将bytecode存储在链上,并获得合约地址调用智能合约的流程:
发起一笔指向智能合约地址的交易,智能合约代码分布式地运行在网络中每个节点的以太坊虚拟机中,然后会获得交易的回执。回执保存交易的输入参数、输出、执行状态等。
举一个例子,首先在Remix平台编写一个智能合约Hello.sol
pragma?solidity?>=0.4.21;contract?Hello{????string?message;????event?SetMessage(string?_message);????function?set(string?memory?_message)?public?{????????message?=?_message;??????????emit?SetMessage(_message);????}????function?get()?public?view?returns(string?memory){????????return?message;????}}
编译成字节码,发起一笔交易部署到区块链中,得到交易回执。
这样就成功的将合约部署到区块链网络中了。
ABI是什么
上文提到调用智能合约,需要发起一笔指向合约地址的交易,以太坊节点会根据输入的信息,选择要执行合约中的哪一个函数和函数的参数。如何知道智能合约提供哪些函数以及参数要求呢,就需要用到ABI了。
合约ABI是在以太坊生态系统中与合约进行交互的标准方法,既可以从区块链外部进行,也可以用于合约间的交互。
ABI类似程序中的接口文档,描述了字段名称、字段类型、方法名称、参数名称、参数类型、方法返回值类型等。
通俗的解释:
ABI是合约接口的说明ABI定义与合约进行交互的数据编码解码规则以之前的Hello.sol为例,在编译合约的时候可以生成合约的ABI
,????????"name":?"SetMessage",????????"type":?"event"????},????{????????"inputs":?,????????"name":?"get",????????"outputs":?,????????"stateMutability":?"view",????????"type":?"function"????},????{????????"inputs":?,????????"name":?"set",????????"outputs":?,????????"stateMutability":?"nonpayable",????????"type":?"function"????}]
5月比特币、以太坊期货和期权交易额均下降:6月3日消息,The Block Pro数据显示,5月比特币期货未平仓量增加2.9%,以太坊期货未平仓量增加5.7%。比特币期货月交易额减少15.3%至7785亿美元。此外,CME比特币期货持仓量减少8.4%至18.5亿美元,日均成交金额减少30.1%至12.2亿美元。以太坊期货月交易额减少24.3%至4080亿美元。
在加密货币期权方面,5月比特币期权未平仓量减少10.6%,以太坊期权未平仓量增加5.6%。比特币和以太坊期权月交易额均下降,其中比特币期权交易额减少12%至168亿美元,以太坊期权交易额减少8.5%至107亿美元。[2023/6/3 11:55:47]
ABI各参数的意义
??name:函数名称
??type:方法类型,包括function、event等
??payable:布尔值,表明方法是否可以接受ether
??stateMutability:状态类型,包括pure(不读取区块链状态),view(和constant类型,只能查看,不会修改合约字段),nonpayable,payable。
??inputs:数组,描述参数的名称和类型
–name:参数名称
–type:参数类型
??outputs:和inputs一样,如果没有返回值,缺省是一个空数组
当用户调用一个合约时,要对调用的函数名和传入的函数参数进行编码,这样EVM才能执行,知道用户调用的是哪个接口,以及正确读取用户的参数,下面介绍以太坊是如何生成可供EVM调用的字节码的。
生成的字节码主要分两部分:函数选择器和参数编码。
函数选择器?FunctionSelector
函数调用的调用数据的前四个字节指定要调用的函数。它是通过将函数签名进行Keccak-256哈希运算后,取前四个字节得到的。
以Hello.sol为例,set函数的接口定义为:
functionset(stringmemory_message)public;
在python3环境下安装ethereum库
>?from?ethereum.utils?import?sha3>?sha3("set(string)").hex()'4ed3885e778f096a5fd9407b264b5478208ea71532d13d454b0307e5f1542101'>?sha3("set(string)").hex()'4ed3885e'
取前四个字节即:4ed3885e
参数编码ArgumentEncoding
从第五个字节开始,后面是编码参数。参数的编码根据类型的不同,编码方式也有所区别。主要分为固定类型和动态类型。
1、固定类型
?uint:M位的无符号整数类型,0<M<=256,M%8==0,如uint32,uint8,uint256.。
?int:M位的两个补码有符号整数类型.0<M<=256,M%8==0.
?uint和int:整型,分别是uint256和int256的别名。注意:函数参数类型是uint,转sha3码时要变成uint256。
?address:地址,20个字节,160bits,等同于uint160。
?bool:布尔类型,1个字节,true:1,false:0
?bytes:固定大小的字节数组,0<M<=32,byte都是bytes1的别名。
2、动态类型
IoTeX成功激活主网v1.2,完成升级全面兼容以太坊:官方消息,新一代高性能公链IoTeX宣布,主网在区块高度11,267,641完成硬分叉,成功激活主网v1.2版本。IoTeX网络已全面兼容支持以太坊工具Web3.js和各类Dapp。用户可以通过以太坊生态工具Metamask直接存储接收发送IoTeX主网通证IOTX和其他XRC20通证。
IoTeX作为硅谷开源项目成立于2017年,以链接现实世界和数字世界为愿景,是与以太坊全兼容的高性能公有区块链。[2021/5/25 22:41:41]
?bytes:动态分配大小的字节数组
?string:动态大小UTF8编码的字符串
?:给定类型的元素的可变长度数组。
?:给定类型的元素的定长数组。
编码规则
固定类型的编码就很简单,直接将参数值转成32字节长度的16进制即可。需要注意的是:数字类型,不足32bytes时,如果是正数高位补0,如果是负数高位补1。布尔类型高位补0。字节类型、字符串类型在低位补全。?动态类型的编码稍微复杂点,如果是固定长度就不需要计算偏移量,如果是不定长度就需要先计算偏移量,并在最后加上长度和具体值的编码。下面举例说明。
Example
给出如下合约,参考官方文档:
https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector-and-argument-encoding
pragma?solidity?>=0.4.16?<0.8.0;contract?Foo?{????function?bar(bytes3?memory)?public?pure?{}????function?baz(uint32?x,?bool?y)?public?pure?returns?(bool?r)?{?r?=?x?>?32?||?y;?}????function?sam(bytes?memory,?bool,?uint?memory)?public?pure?{}}
案例1
函数:baz(uint32,bool)?
调用:baz(69,true)
?0xcdcd77c0:函数选择器,在python中通过sha3("set(string)").hex()得到0xcdcd77c0
?0x0000000000000000000000000000000000000000000000000000000000000045,十进制69,转成16进制为45,因为是正数,高位补0至32bytes
?0x0000000000000000000000000000000000000000000000000000000000000001,bool类型,true=1,false=0,高位补0
最终的字节码为
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
会返回bool类型.在这个调用中,返回值是false,它的输出将是单字节数组
0x0000000000000000000000000000000000000000000000000000000000000000
案例2
函数:bar(bytes3memory)?
调用:bar()
?0xfce353f6:函数选择器,在python中通过sha3("bar(bytes3)").hex()得到0xfce353f6
Bitcoin Suisse:以太坊2.0抵押服务的推出推动11月交易量大幅上涨:近日,瑞士加密资产经纪商Bitcoin Suisse表示,其帐户数量在11月份增长了15%,而交易量则翻了一番。以太坊2.0抵押服务的推出推动了交易量的增长,其银行担保也增加到了6000万瑞士法郎(约6747万美元)。(Finance Magnates)[2020/12/8 14:37:02]
?固定长度不需要计算偏移量
?0x6162630000000000000000000000000000000000000000000000000000000000,字符串abc转成16进制后为616263,低位补0
?0x6465660000000000000000000000000000000000000000000000000000000000,字符串def转成16进制后为646566,低位补0
字符串转16进制的python参考代码
import?binasciis?=?'abc'str_16?=?binascii.b2a_hex(s.encode('utf-8'))??#?字符串转16进制print(str_16)
案例3
函数:sam(bytes,bool,uint)
调用:sam("dave",true,")
?a5643bf2:函数选择器,在python中通过sha3("sam(bytes,bool,uint256)").hex()得到a5643bf2.请注意,将uint替换为其规范表示形式uint256。
?0x0000000000000000000000000000000000000000000000000000000000000060:动态类型,计算偏移量。这个的偏移量是指实际存储值的位置,由于这个函数有3个变量,那么实际存储值的位置就是第四个32bytes位置,也就是说偏移量等于3*32bytes=96,转成16进制后就是对应的值
?0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数,布尔值true
?0x00000000000000000000000000000000000000000000000000000000000000a0:动态类型,计算偏移量,这个偏移量就等于参数长度3*32bytes+前面的动态参数参数占有的长度,那么具体的值就是3*32bytes+(1*32bytes+1*32bytes)=5*32bytes=160,转成16进制就是a0,高位补全就是对应的值
?0x0000000000000000000000000000000000000000000000000000000000000004:第一个参数的数据部分,代表元素中字节数组的长度,在这种情况下为4。
?0x6461766500000000000000000000000000000000000000000000000000000000:“dave”的utf-8编码,填充为32字节。
?0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的数据部分,代表数组中元素的个数,在这种情况下为3.
?0x0000000000000000000000000000000000000000000000000000000000000001:第三个参数的第一项。
?0x0000000000000000000000000000000000000000000000000000000000000002:第三个参数的第二项。
Sovos解决方案负责人:美国政府应阐明以太坊质押相关的加密税收政策:金色财经报道,美国政府尚未阐明其对质押加密货币的税收政策。根据软件公司Sovos解决方案负责人Wendy Walker的说法,质押以太坊后,在收到新的加密货币或销售或交易这些代币时可能有税收义务,但用户并不清楚具体的细节。[2020/6/30]
?0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的第三项。
最终的字节码为
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
综上所述,ABI是合约接口的说明,并定义了与合约进行交互的数据编码解码规则。
事件和日志
区块链是一个块列表,从根本上讲就是交易列表。每一个交易都有一个收据,其中包含0个或多个日志记录。日志记录表示从智能合约触发的事件的结果。在以太坊中,事件是一个基本功能,可以将数据记录成日志,保存在区块链上。事件也可以与外部交互,比如与前端进行交互。事件强调功能,是指触发操作的行为,日志强调存储,是指触发事件后,将数据保存在区块链上,形成日志。
事件如何定义和触发
在solidity中,使用关键字event来定义事件,使用关键字emit来触发事件,其参数列表就是需要保存在区块链上的数据,最多可有三个具有indexed属性的参数,表示其可以被索引,便于查找。
contract?MyContract{????event?Transfer(address?indexed?from,?address?indexed?to,?uint256?value);????function?transfer(address?_to,?uint256?_value)?public?returns?(bool)?{????emit?Transfer(msg.sender,?_to,?_value);????return?true;??}}
事件的作用
事件可以在不同的场景下使用,主要有如下三种作用。
获取合约执行结果过滤日志存储合约数据1、获取合约执行结果
在开发Dapp时,我们会通过发送一笔交易来调用智能合约的某个函数,但是我们不能立即得到返回值。因为交易不是立刻打包进区块链的,在这种场景下,可以使用事件来解决这个问题。
以和前端交互为例,我们可以通过编写代码来监听某一特定事件来做到更新前端。例如通过如下代码来监听上文提到的合约中的Transfer事件。
以太坊基金会获得超250万美元赠款:据Coindesk消息称,以太坊基金会正式宣布接受了其他机构对其进行的第一批赠款,总额超250万美元。共有13个项目获得数额不等的资金,用于可扩展性、安全性、开发经验、用户界面研究以及以太坊区块链的其他衍生产品。以太坊基金会是致力于推进和维护以太坊软件的非盈利组织。[2018/3/9]
var?event?=?myContract.Transfer();event.watch(function(error,?result){????if?(err)?{????console.log(err)????return;??}??console.log(result.args._value。);
当调用transfer函数的交易被打包进区块链中时,将会触发回调中的watch函数,前端可以得到有效的transfer函数的返回值。
事件通常可以被看作带有数据的异步触发器。当一个合约想要触发前端时,合约会发出一个事件。因为前端正在监听这个事件,一旦监听到相关事件,前端可以采取相应的操作,比如显示消息,更新前端展示内容等。
2、过滤日志
日志不能被合约访问,Solidity没有提供查询日志的接口,在监听日志的时候,Solidity提供了filter功能,借此我们可以实现对日志的查找过滤。在Transfer事件中,from和to参数被设置成indexed,说明其是可以被索引的。所以我们可以监听特定的事件,例如转账地址为0xab213的事件,也可以监听从0xab213地址转账到0x417ac的事件,但由于value参数没有indexed属性,所以我们不能监听例如value为100的事件。
在此场景下,我们如果想过滤指定地址发出的交易,我们可以通过web3.js编写如下代码。
Mycontract.deployed().then(function?(instance)?{var?event?=?instance.Transfer({}function?(error,?result)?{????var?obj1?=?{????????'_to':?'0xab213',????}????var?obj2?=?{????????'fromBlock':?0,????????'toBlock':?'latest'????}????var?event?=?instance.Transfer(obj1,?obj2)????event.watch(function?(error,?result)?{????????console.log(JSON.stringify(result))????}。).then(function?(value)?{????console.log(value。).catch(function?(e)?{????console.log(e。)
参数说明:
?obj1:添加indexed属性的参数,在这里我们可以过滤特定地址0xab213发起的交易
?obj2:Solidity提供的额外的过滤参数,可选的主要参数有:
–fromBlock:指定过滤的起始位置,值为块的编号,默认为latest
–toBlock:指定过滤的结束位置,值为块的编号,默认为latest
·callfunction:回调函数function(error,result
3、存储合约数据
与上面讲述的不同,事件可以作为便宜的多的一种存储形式。通过触发事件,存储在日志上的数据,基本上每字节花费8gas,但是智能合约每存储32字节花费20000gas。尽管日志可以节省大量gas,但是无法从任何智能合约中读取日志信息。需要根据使用场景来选择适合的存储办法,日志作为一种廉价存储的方式,适合存储可由前端展示的历史数据。
日志记录的组成
EVM具有5个用于发出日志的操作码:LOG0,LOG1,LOG2,LOG3和LOG4。通过这些操作码来创建日志记录。
每个日志记录都包含topics和data。Topics是bytes32类型的参数,不同的操作码描述包含在日志记录中的Topics数量。LOG1包含1个topic,LOG2包含2个topics,最多支持4个topics。
Topics用于描述事件,日志中存储的不同的具有indexed属性的事件就叫不同的主题。比如对于事件Transfer来说,其定义为eventTransfer(addressindexedfrom,addressindexedto,uint256value);,有三个主题。第一个主题为事件签名的哈希值,即通过keccak256("Transfer(address,address,uint256)")来得到,如果该事件是匿名事件,那么就没有这个主题。后面两个具有indexed属性的参数,可以用来进行过滤进行精确查找。
由于只能容纳32个字节的数据,所以无法将数据或字符串之类的参数用作Topic,应该将其作为data包含在日志记录中。如果想要包含超过32字节的topic,应该将其哈希。Topic可视作事件的索引,其使用场景在于可以有效缩小搜索查询范围内的数据。
日志记录的另一个部分是数据。Topic是可以搜索的,但是数据却不可以,数据可以摆脱Topic的32字节大小的限制,包含例如数组或字符串的复杂数据。
下面举一个例子来说明。还是以上文中的Transfer事件为例,由于Transfer不是一个匿名事件,所以第一个Topic包含事件签名。
contract?MyContract{????event?Transfer(address?indexed?from,?address?indexed?to,?uint256?value);????function?transfer(address?_to,?uint256?_value)?public?returns?(bool)?{????emit?Transfer(msg.sender,?_to,?_value);????return?true;??}}
在事件的参数部分,Transfer事件有三个参数from、to、value,其中from和to被声明为indexed,标识其被视为topic,value参数不会被索引,将会作为日志的数据部分。此事件包含3个topics,将使用LOG3操作码来创建日志记录。
举个例子,将上述示例合约部署到区块链网络中,我们使用:0x246B0ED379bdDbe1aDaC56277Ce5cB3018c24E04地址调用transfer函数,to参数指定为0x3F0b9B0D373C26328A879430383e87F4780AD410,value指定为1,发起一笔交易获得交易的回执信息。其中在logs属性中,记录如下信息
,????"type":"mined",????"id":"log_cf046b97"??}]
其中,topics数组中有三个元素,第一个为keccak256("Transfer(address,address,uint256)")的结果,第二个为from参数的值,这里为0x246B0ED379bdDbe1aDaC56277Ce5cB3018c24E04,第三个为to参数的值,这里为0x3F0b9B0D373C26328A879430383e87F4780AD410。data字段是通过对value进行编码得到的,这里将1转成32字节长度的16进制的值,得到0x0000000000000000000000000000000000000000000000000000000000000001。
布隆过滤器LogBloomandFilter
布隆过滤器在以太坊中用于检索交易日志log,方便交易结果的查询以及交易事件通知。在以太坊的区块头中,有一个区域叫做logsBloom。这个区域存储了当前区块中所有收据的日志的布隆过滤器,有2048个bit,相当于256个字节。在一个交易的收据中,可能存在0个或多个日志记录,每个日志记录中包含了相应的Topics和data。在一个交易的收据中同样也存在布隆过滤器,记录了所有的日志记录数据。
下面介绍布隆过滤器的原理。我们知道,查找一个元素是否存在于一个集合中,可以使用数组这种数据结构。但是当数据量非常庞大的情况下,数组的空间开销和查询开销也会变得非常大。
布隆过滤器的原理是当一个元素被加入到一个集合中时,使用K个哈希函数,对该元素求哈希值,得到K个不同的哈希值,分别记作X1,X2,X3,…,XK。将这K个数字作为位图的下标,将对应的
BitMap,BitMap,BitMap,…,BitMap都设置为1。即用K个bit来表示一个元素是否存在。
当想要查询某个元素是否存在于这个集合中时,用相同的K个哈希函数,得到Y,Y,Y...Y,如果这K个哈希值对应的位图下标均为1,则表示这个元素可能存在,如果有任意一个下标不为1,则说明这个元素肯定不存在。
举例说明,假设集合中现在有3个元素{X,Y,Z},使用3个哈希函数。首先将数组进行初始化,每一位置为0。然后对于集合中的每一个元素,都通过3个哈希函数进行哈希计算,每次计算都会产生一个下标,将数组中该下标对应的值设置为1。这时候想要查询一个元素W是否在该集合中,依次用这3个哈希函数将W映射到数组的3个位点上,如果3个位点对应的值都为1,则可能存在于集合中,若有一个位点的值不为1,则该元素一定不在这个集合中。
布隆过滤器存在一定的误判性,因为存在一定的哈希碰撞可能,所以当位点上的值都为1时,只能反映该元素可能存在于该集合中。但这不影响其广泛的应用性,布隆过滤器是一种存储效率很高的数据结构,其空间利用率和时间利用率非常高,插入数据和查询数据的时间复杂度为O。
布隆过滤器可以用于事件查询,在以太坊中,系统会先创建各个主题的布隆过滤器,然后通过合并获得事件的布隆过滤器,再次合并得到交易的布隆过滤器,最后合并得到区块的布隆过滤器。在查询过程中,查询满足指定特征的事件的过程是,先根据查询条件得到布隆过滤器,如果其位向量是区块布隆过滤器的子向量,则可能存在此区块中,如果不是其子向量,那么就不存在此区块中。如果可能存在,继续比对区块中各个交易的布隆过滤器,比对交易中每个事件的布隆过滤器。如果与事件的布隆过滤器匹配,再进行严格的数据验证,相同的话即说明存在。
参考资料
https://academy.binance.com/zh/articles/what-are-smart-contracts
https://segmentfault.com/a/1190000016634359
https://cloud.tencent.com/developer/article/1328286
https://fisco-bcos-documentation.readthedocs.io/zhCN/latest/docs/articles/3features/35contract/abiof_contract.html
http://www.jouypub.com/2018/437e42a5629ea0ccd567909c94abb4a4/
https://media.consensys.net/technical-introduction-to-events-and-logs-in-ethereum-a074d65dd61e
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。