前段时间,Jarvis+在批量发送Jarvis+的奖励JWT的过程中遇到了在区块链浏览器上面批量交易显示部分成功的问题,交易显示成功了,JWT却没有到账,究竟是什么原因呢?下面由Jarvis+的Michael为大家科普:NEO区块链浏览器到底出现了什么问题?又是怎么发现的?
问题现象
neoscan和neotracker是NEO社区最受欢迎的两款区块链浏览器。最近在一笔交易中,Jarvis+发现两款浏览器都存在同一个问题。
在一笔NEO 交易中同时打包多笔转账,比如NEO交易收据值为:
0x2326eb9e7d5dda11ea71c9b4428f4fa036003066134fce8d93195732514ed488 的这一笔交易。在该交易中,一共打包了5笔转账。在该笔交易中,分别向5个不同的地址分别打入了999个JWT资产。在转账结束后,neotracker 和neoscan 两款主流的NEO区块链浏览器上仅可以查询到3笔转账记录,而不是5笔。虽然3笔转账记录上显示已经为地址打入999个JWT资产,但是当使用这3个接收JWT 资产的地址中的任意一个向外继续转账JWT 资产时,会因为余额不足而导致转账失败。
问题分析
下面,neotracker 这款区块链浏览器来查看和分析一下问题。该笔交易的详情,请访问:
https://neotracker.io/tx/2326eb9e7d5dda11ea71c9b4428f4fa036003066134fce8d93195732514ed488
如下图所示,这笔交易是在2018年7月27日下午生成的,交易信息由570个字节组成,被打包在第2551742个NEO区块内。在Transfers一栏中,可以看到一个地址AKNmRJUiB3pwFgNrVYuQ4LdAsfRS9RuKiu,分别给另外三个地址打入了999个JWT资产。
事实真的如此吗?为了继续分析问题,直接在当前交易详细信息页面上打开Script 一栏进行查看,Script 中显示的内容就是打包上传到区块链上的交易字节信息,一共是570个字节。详细信息显示如下:
0000:PUSHBYTES8 0×0007814217000000
0009:PUSHBYTES20 0x3c2ecf82be1671982a6cf420845f96003ad12a85
0030:PUSHBYTES20 0x2781bccf8be5f34c88f904b48a7e478f07c5e41f
0051:PUSH3
0052:PACK
0053:PUSHBYTES8 0x7472616e73666572
0062:APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa
0083:PUSHBYTES8 0×0007814217000000
0092:PUSHBYTES20 0xfead57b656edda0dbecc5d54b4130cba91dbbf54
0113:PUSHBYTES20 0x2781bccf8be5f34c88f904b48a7e478f07c5e41f
0134:PUSH3
0135:PACK
0136:PUSHBYTES8 0x7472616e73666572
0145:APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa
0166:PUSHBYTES8 0×0007814217000000
0175:PUSHBYTES20 0x0272b6b14a339e02b78799acc3e2b3825e25564c
0196:PUSHBYTES20 0x2781bccf8be5f34c88f904b48a7e478f07c5e41f
0217:PUSH3
0218:PACK
0219:PUSHBYTES8 0x7472616e73666572
0228:APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa
0249:PUSHBYTES8 0×0007814217000000
0258:PUSHBYTES20 0x4a866e47b8974937279a405c31c898da6a7adfd1
0279:PUSHBYTES20 0x2781bccf8be5f34c88f904b48a7e478f07c5e41f
0300:PUSH3
0301:PACK
0302:PUSHBYTES8 0x7472616e73666572
0311:APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa
0332:PUSHBYTES8 0×0007814217000000
0341:PUSHBYTES20 0xb18b4948eaabcc8bde828e005a0554d09e4ecd58
0362:PUSHBYTES20 0x2781bccf8be5f34c88f904b48a7e478f07c5e41f
0383:PUSH3
0384:PACK
0385:PUSHBYTES8 0x7472616e73666572
0394:APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa
先看第一行,PUSHBYTES8 0×0007814217000000 按照NEO 虚拟机的指令,这是把一个8字节的数字压栈。0×0007814217000000 是按照从低位字节到高位字节的顺序排列的。实际上,这个数字应该是十六进制的0×1742810700,也就是十进制的99900000000。由于JWT 的精度是8位,所以这个数字实际上代表的是999个JWT。
第二行PUSHBYTES20 0x3c2ecf82be1671982a6cf420845f96003ad12a85 是往NEO虚拟机中压栈了20个字节的数组,这个数组实际上就是NEO地址对应的公钥,根据这个公钥可以反算到NEO的地址。同样的,第三行也是一个地址,是用来发放JWT 的NEO地址。
指令PUSH3 用来告诉NEO 虚拟机这次智能合约的调用有三个参数,也就是刚才压入堆栈的三个参数。PACK 告诉NEO虚拟机把这三个参数打包。
随后的指令PUSHBYTES8 0x7472616e73666572 是入栈了一个8个字节的数组。这个数组实际上是一个字符串,也就是” transfer”。即告诉NEO虚拟机调用智能合约中的transfer 方法。
第七行,APPCALL 0xb4cf9f4af5272d209b80ad1a06885e21568a4bfa 是告诉NEO 虚拟机要调用的智能合约的地址。
以上的OpsCode 形成了这个交易中的一笔转账,近似内容重复了5次,也就是标明这笔交易打包了5笔转账操作。
那为什么在交易的详情中只能看到三笔交易呢? 这就是亟待研究的问题了。
根本原因
很显然,既然是同样的转账操作,每笔转账操作除了接收JWT 的地址不一样以外,其他部分都完全一样。这包括调用的智能合约地址、转账方法、转出JWT 资产的地址和转的JWT资产的数量。在这种情况下,有些转账能够成功,有些转账不成功。就要从其他方面入手了。
可能一:用于转账的JWT 地址没有足够的JWT 资产额度。
经过确认,改地址是有充足的JWT 资产用于转账的。通过NEP-5 合约的balanceOf() 方法就可以查询到。
可能二:用于执行转账的Gas 不足。
在NEO 链上,执行智能合约是需要消耗一定的Gas的,Gas的消耗量与智能合约执行的时间成正比。为了方便简单的智能合约调用,NEO允许每次执行合约时给出免费Gas 的额度。这样一来,即使是一个不含有任何资产的NEO 地址也可以调用简单的智能合约了,降低了智能合约的使用门槛。这个免费的额度可以通过查询NEO 源代码了解到。参见代码:
https://github.com/neo-project/neo/blob/master/neo/SmartContract/ApplicationEngine.cs#L45
可以了解到Gas 的免费额度是10。
现在就要看看这笔交易到底能消耗掉多少Gas了。实际上智能合约Gas 的计算是根据智能合约使用的系统调用方法的价值和次数来确定的。统计调用智能合约消耗的Gas 代码在ApplicationEngine.cs 文件的322 -327 行。可以访问下面的链接直接查看:
https://github.com/neo-project/neo/blob/master/neo/SmartContract/ApplicationEngine.cs#L322-L327
在代码中,执行引擎逐个遍历系统调用指令,然后根据每个系统调用指令的单价乘以费率进行累加,得到该交易的Gas 消耗量。
费率(ratio) 是一个固定值,通过查询NEO 源代码可以知道是100000。由于Gas 的精度也是8位,所以这个值代表0.01个Gas。每个系统调用的价格是在GetPrice函数中确定的。代码在ApplicationEngine.cs 的351-380行。可通过下面的链接进行查看:
https://github.com/neo-project/neo/blob/master/neo/SmartContract/ApplicationEngine.cs#L351-L380
如果仔细看一下,发现其实执行智能合约好像并不贵。PUSH压栈操作一律免费(return 0),调用智能合约是10,也就是0.1个Gas。5笔转账加在一起,消耗的Gas并没有多少。但是,这里有一个隐含的调用,就是验证签名。验证签名是用来在transfer 方法中校验当前发起转账调用的一方是否就是当前转出JWT 资产的地址的所有者。这是处于安全的考虑,如果不对智能合约的发起方做这样的调用,那么就会出现不具备JWT 资产的第三方C掏一点点Gas 就把拥有JWT 资产的A的JWT 资产莫名转给了B。因此,在智能合约的转账操作中,校验执行合约发起方的签名是绝对有必要的!校验签名收费1个Gas(100 * 100000)。5笔转账操作单是校验签名就已经消耗掉一半的免费额度了。实际上,一笔转账操作所花费的Gas大约是2.742,5笔转账操作加在一起就超过10个Gas 的免费额度了。
这笔交易其实是执行失败了。失败的原因是调用智能合约时没有足够的Gas。免费的Gas额度仅够3笔转账操作的。
按照正常的NEO 处理交易的规则,一旦交易中途执行失败,就应该返回FAULT_BREAK错误提示,交易结果数据不能打包上链。但是由于neotracker 和neoscan 两个区块链浏览器没有正确地处理该笔交易,导致区块链浏览器显示该笔交易中的前三个转账操作成功,后两个转账操作失败(不显示)。
问题解决
这个问题出现以后,引起Jarvis+的高度重视。及时地分析了问题,并对转账失败的5个地址补发了JWT 资产。
从这个问题上可以得出下面的最佳实践:
在免费的资产转帐操作中,一次最好不要同时给三个以上的地址进行转账。否则,最好是在调用资产转账智能合约时给予足够的Gas额度。
对于NEO区块链浏览器neoscan 和neotracker 来说,需要他们及时地升级NEO 节点到最新版本。避免类似情况再次发生。
Jarvis+已经在Github上向两款NEO区块链浏览器提交了这个问题。详情请参考下面链接地址:
向neoscan 提交的问题链接:https://github.com/CityOfZion/neo-scan/issues/322
向neotracker 提交的问题链接:https://github.com/neotracker/neotracker/issues/53
有任何问题,都可以通过官方渠道联系我们哦~