【大神专栏】是临界 (Hashgard) 的新栏目,我们会在团队、投资人、战略顾问中寻找大神,每一期邀请一位大神,撰写一篇专栏文章,为临界 (Hashgard) 的小伙伴们答疑解惑
本期大神专栏
由技术大神【Jason】解答
以太坊上如何实现批量转账功能
- 01 -
ETH拥堵困扰 - 交易机制
ETH 作为目前应用最广的智能合约网络,受制于TPS(当前ETH 网络出块速度为14秒/块,每区块包含约140笔交易,即每秒10笔交易)的约束时常导致网络拥堵,因此开发者在设计之初就要考虑到拥堵状况及交易手续费。
在ETH上发起一笔交易,矿工收取手续费后将该交易打包到下一个区块,便于理解我们用生活术语解释整个过程。
工作量 = 油钱
Gas Limit = 汽油量(固定)
Gas Price(单位:Gwei) = 汽油单价
矿工 = 加油站老板
一笔交易的手续费=工作量(油钱)= Gas Limit(汽油量)* Gas Price(汽油单价)
由于每个区块的处理数量有限,为了利益最大化,矿工会优先处理支付Gas Price更高的交易。通常ETH会推荐一个Gas Price,确保合理的价格和快速交易。但随着处理交易增多产生拥堵,用户会大幅度提高Gas Price ,来进行合理的「插队」,随着插队人数越来越多, Gas Price 价格会一路走高,拥堵高峰时,Gas Price会达到平日的十倍甚至是百倍
上图为监测到的Gas Price数据,下方的深蓝线和上方的绿色线分别是来自于etherscan.io的Safe Price和Propose Price, 中间的蓝绿线则是ETH节点默认配置下给出的Standard Gas Price(节点计算最近的一定数量的区块中所有交易的平均Gas Price,再乘以一个系数,结果即是Standard Gas Price)。Safe Price由etherscan.io的节点计算而出,在发送交易设置时,设置的Gas Price不要低于Safe Price,否则有可能在etherscan.io的交易池中暂时找不出这笔交易的详细信息。Standard Gas Price通常要高于etherscan.io给出的Safe Price,是相对合理的价格,发送交易时把Gas Price设为它,交易通常在数分钟内到账。etherscan.io给出的Propose Price则是激进的价格,交易会在几个块之内到账。
- 02 -
ETH拥堵困扰 - 批量发送
在糖果盒子,交易所等需要批量交易的场景时,虽然可以通过累加nonce值来一次性发出多笔交易,但发出的交易只是进入未确认交易池,而只有被打包进区块的交易才是成功到账,这些多笔交易何时被打包取决于网络拥堵状况和用户设置的Gas Price。
这里解释下ETH中的nonce,ETH账户体系机制决定了,从任一地址发出的交易都会在交易信息里设置一个顺序编号,编号从0开始,该地址发出的下一笔交易被打包进区块时,编号就要+1,以此来记录交易历史排序和数量,这套编号系统就是nonce。假设地址A之前一共发送过9笔交易都成功到账,A要发第十笔,交易里的nonce便要设为9(从0开始计算)。如果设置成10,矿工会一直等待nonce为9的交易出现,而这笔nonce为10的交易无法打包进区块的。即矿工只能按nonce数值顺序强制处理交易。而如果有两笔同样nonce为9的交易,节点会选择交易费更高交易,另一笔则不会处理。
如图所示,在由地址A发出的且已经被打包进区块的交易已有10笔,nonce的值为9(从0开始)。所以交易池中nonce为10,11,12的交易接下来都有可能按序被矿工打包,这些未确认交易处于pending状态。但因为缺少nonce为13的交易,那nonce为14的交易暂时不可能被矿工列入打包的候选交易,处于queued状态。待nonce为13的交易出现在交易池中时,nonce为14的这笔交易才有可能被打包。
这导致,如果某一地址批量发送,只要有一笔交易Gas Price 过低,根据nonce 机制,系统将不会处理这币交易nonce值后面的交易,造成批量发送停滞。
- 03 -
拥堵时可以采取的策略
ETH网络出现拥堵时,通常采取「多批量多地址」的方式应对,设置一个水位,当某个地址在交易池的pending队列中有50笔未确认交易时,则停止用该地址发送交易,等待这50笔未确认交易被矿工打包后,再重新开启。并使用多个热钱包地址去发出交易,避免单个地址被其某笔交易堵塞而导致整个功能模块暂停。此外,若某笔未确认的交易阻塞已久,可以重新发出这笔交易并设置并设置更高更合理的Gas Price,矿工节点会用这笔新的交易替换掉原先被阻塞的,往往能解决队列拥堵的问题,这种动态补偿机制也为诸多交易所,钱包所使用。
此方案是为了在ETH拥堵时保证应用的正常运行和用户体验,本质上提高了Gas Price,加大了手续费的消耗。如果有办法减少单笔交易的Gas消耗,也就变相降低了手续费。
- 04 -
合约层面的批量转账
假设从一个地址往10个地址发送某ERC20 Token,目前方法是针对每个地址去创建一笔transfer交易,批量产生了10笔交易然后发出。 如果能像BTC那样,在一笔交易里写入多笔转账操作,就能降低的Gas消耗
一些Token在合约里就实现了批量转账的 batchTransfer 方法,该方法接收数组,然后开始循环,针对数组中的每个地址进行发起一笔转账。batchTransfer 并不是ERC20代币的标准,本着越简单越安全的原则,如果要在Token合约内实现该功能,请务必要做好代码的安全审计。让我们看一下BEC合约里对 batchTransfer 的实现:
实现逻辑合理,遗憾的是,BEC的开发团队在这里出了个极其严重的Bug:* 运算后,没有对结果进行溢出判断,存在严重风险,可为攻击者利用来凭空生成代币。
除此之外,还有其他的方式在合约层面实现批量转账
首先,ETH是无法在一笔交易里直接去多次调用合约或者同时调用多个合约的,但在合约内的函数是可以外部调用其他合约。我们可以将批量转账的逻辑以一个智能合约来实现,通过该合约的去多次外部调用Token合约内 transfer 方法,其目的也就达到了,原理如下图所示:
地址B上是一个支持ERC20标准的代币合约,提供有"transfer"方法,我们可以再部署一个中间合约来实现批量调用的逻辑,中间合约里的"batchTranfer"接收ERC20代币的合约地址,代币接收方地址的数组,与之相对的token转账数量数组。在该方法对接受方数组里的每个接受者,外部调用Token合约(address B)上的”transfer”函数, 实现代码:"_token.call(bytes4(keccak256("transfer(address,uint256)"), receivers[index], amounts[index])"。这里使用到了"call",这是solidity中很底层的一个方法,使用不当会出现安全隐患,请先去足够了解有关"call"的更多细节!
需要注意,在中间合约(address A)外部调用Token合约(address B)的"transfer"时,由于”call”的特性,msg.sender会从起始的address C 变成 address A,其实是从address A中转出Token,那就要先往address A中转入足够的Token。为了保障中间合约(address A)上Token的安全,合约必须要设置一个owner,”batchTransfer”内一定要有检查address C调用者是否与owner一致的逻辑。owner为address C, 只有address C能调用这个中间合约,且整个交易的手续费从address C扣除。
实现时还需要具体考虑安全和性能的问题,比如_receivers数组和_amounts数组的长度是否相等,循环中”call”调用失败后应该回退整个批量交易,避免传入的数组长度过大等等。
来看看该方案实际的效果:
下图是从某地址去调用Token合约内 transfer 方法的消耗:
下图是我们上面实现的,通过 BatchTransfer Caller 这个中间合约去调用Token合约的 transfer ,这里先我们只往参数数组里传入1个接收者:
因为多了一些中间操作,参数结构也更加复杂,当传入数组长度为1时,这样比直接调用Token的 transfer 的Gas的消耗反而增加了一些。再看看往数组里塞入10个接受者时的情况:
此时,与直接去调用Token的 transfer 方法10次的消耗量相比,通过 BatchTransfer Caller 合约来实现批量转账节省了大概30%的Gas总消耗,是不是非常可观呢。
- 05 -
总结
在ETH网络拥堵时,如果要批量发起交易,首先要考虑操作的安全问题,在交易确认速度和Gas Price中寻找一个平衡点,然后通过一些动态的调整机制以及合约的外部调用来保障交易的确认速度以及减少手续费消耗。
- 06 -
关于临界(Hashgard)
临界(Hashgard)是由GF Network旗下分布式资本、BKFUND领投,80家机构跟投的新一代数字金融公有链,将为种类繁多的金融业务提供一站式的区块链解决方案,赋能给优秀的个体比如基金经理、产品经理、交易员、投资经理、风控经理在链上相互连接创造出丰富的金融产品,同时也能够为全球的用户提供便捷、安全、可信的金融服务。
网友评论