大龍說(shuō)幣-ETH的實(shí)踐

ETH擁堵對(duì)于批量發(fā)送交易帶來(lái)的困擾

對(duì)于ETH應(yīng)用開(kāi)發(fā)者而言,ETH網(wǎng)絡(luò)的擁堵?tīng)顩r,交易手續(xù)費(fèi)都是在設(shè)計(jì)開(kāi)發(fā)時(shí)必須要考慮的點(diǎn)。

先來(lái)了解下ETH處理交易的性能:ETH理論的交易處理速度是每秒30筆,但目前的出一個(gè)塊的平均時(shí)間大概是14秒,平均每個(gè)塊包含一百多筆交易,也就是說(shuō),目前ETH每秒能打包的交易大概是10筆。

在ETH上發(fā)起一筆交易,礦工將這筆交易打包到新的區(qū)塊中是需要收取手續(xù)費(fèi)的, 手續(xù)費(fèi)又由哪些因素決定呢?先來(lái)了解下ETH網(wǎng)絡(luò)中的Gas(燃料):

Gas用來(lái)衡量執(zhí)行交易需要多少“工作量”,這些“工作量”就是為了執(zhí)行該動(dòng)作支付給ETH礦工的費(fèi)用額。當(dāng)發(fā)起某筆交易時(shí),發(fā)送方設(shè)定了Gas Limit 和 Gas Price。Gas Limit表示發(fā)送方愿意為了執(zhí)行這筆交易最多付出的Gas數(shù)量,而Gas Price是發(fā)送方給單位Gas設(shè)置的價(jià)格。用戶發(fā)起一筆交易好比發(fā)出一趟車(chē),執(zhí)行交易的復(fù)雜度類(lèi)比成車(chē)要行駛的里程,交易成功好比這輛車(chē)跑完整個(gè)行程。Gas Limit是發(fā)車(chē)時(shí)的給油箱加的油量,Gas Price就是每升汽油的價(jià)格,發(fā)車(chē)前必須先加好油,如果油不夠(Gas Limit不足),這趟車(chē)半路會(huì)拋錨(交易執(zhí)行失敗)。當(dāng)這輛車(chē)跑完行程后,實(shí)際消耗的油量就是Used Gas,郵箱里剩下的油會(huì)按加油時(shí)的原價(jià)返還給用戶。用戶最后支付的油錢(qián)(Used Gas * Gas Price)就是加油站(礦工)所得。大致原理如圖所示:

txfee.jpg

可是礦工在一個(gè)區(qū)塊中打包交易的數(shù)量是有限的(Block Gas Limit), 為了自身利益的最大化,礦工通常會(huì)優(yōu)先打包那些Gas Price更高的交易。我們?yōu)榱耸拱l(fā)出的交易能盡快獲得確認(rèn),會(huì)設(shè)置合理的Gas Price。合理的Gas Price是個(gè)動(dòng)態(tài)的值,節(jié)點(diǎn)計(jì)算最近的一定數(shù)量的區(qū)塊中所有交易的平均Gas Price,再乘以一個(gè)系數(shù),結(jié)果即是Standard Gas Price。在ETH不擁堵的情況下,節(jié)點(diǎn)給到的Standard Gas Price通常在 1~10 Gwei之間,但當(dāng)ETH開(kāi)始變得擁堵,用戶們?yōu)榱俗约喊l(fā)出的交易能優(yōu)先被礦工打包,會(huì)給出更高的Gas Price??傻V工打包的效率并不會(huì)因?yàn)榇蠹叶冀o得高就加快,用戶又紛紛設(shè)定更高Gas Price,節(jié)點(diǎn)給出的Standard Gas Price會(huì)被一路推高。在擁堵高峰時(shí),這個(gè)值會(huì)是平常是數(shù)十倍甚至上百倍之多。這意味在ETH擁堵的情況下,我們要出更多的手續(xù)費(fèi)去和他人競(jìng)爭(zhēng),如果你舍不得錢(qián)的話,設(shè)置的Gas Price不合理,你的交易會(huì)待在交易池里無(wú)人問(wèn)津。

gas.jpeg

上圖為監(jiān)測(cè)到的gas price數(shù)據(jù),藍(lán)線來(lái)自于etherscan.io的Safe Price, 綠線則是ETH節(jié)點(diǎn)默認(rèn)配置下給出的Standard Gas Price。Safe Price由etherscan.io的節(jié)點(diǎn)計(jì)算而出,我們?cè)诎l(fā)送交易設(shè)置時(shí),設(shè)置的Gas Price盡量不要低于Safe Price,要不然在很有可能在etherscan.io的交易池中暫時(shí)找不出這筆交易的詳細(xì)信息。Standard Gas Price通常要高于etherscan.io給出的Safe Price,是相對(duì)合理的價(jià)格,發(fā)送交易時(shí)把Gas Price設(shè)為它,交易通常在數(shù)分鐘內(nèi)到賬。

在開(kāi)發(fā)ETH錢(qián)包,糖果盒子,交易所等會(huì)進(jìn)行批量交易的應(yīng)用時(shí),雖然可以通過(guò)累加nonce值來(lái)一次性發(fā)出多筆交易,但發(fā)出的交易只是被放進(jìn)了未確認(rèn)交易池里而已,只有被打包進(jìn)區(qū)塊的交易我們才能認(rèn)為是成功到賬的,而交易何時(shí)能被打包還取決于當(dāng)前ETH網(wǎng)絡(luò)的擁堵情況,你為交易設(shè)置的Gas Price。這里提一下ETH中的nonce:ETH賬戶體系的是這樣設(shè)計(jì)的,從一個(gè)地址發(fā)出的交易會(huì)在交易信息里設(shè)置一個(gè)順序編號(hào),編號(hào)從0開(kāi)始,每有一筆該地址發(fā)出的交易被打包進(jìn)區(qū)塊,在發(fā)起下一筆時(shí),就要加上1,這個(gè)起到記錄某地址發(fā)送歷史發(fā)送交易數(shù)量的,并能對(duì)過(guò)往交易排序的順序編號(hào)即為nonce。假設(shè)地址A之前一共發(fā)送過(guò)9筆交易都成功到賬,現(xiàn)在又要構(gòu)建一筆由A發(fā)出的新交易,交易里的nonce便要設(shè)為9。如果不設(shè)置成9,而是設(shè)置為10,礦工會(huì)一直等待nonce為9的交易出現(xiàn),那這筆nonce為10是不可能被打包進(jìn)區(qū)塊的。那在發(fā)出nonce為9的交易后,不能這筆交易確認(rèn),接連發(fā)出的nonce為10,11,12的交易,nonce值更高的交易不會(huì)比nonce值低的交易優(yōu)先打包,只可能在nonce更低的叫被打包之后,或者與其同時(shí)被打包。再假設(shè)發(fā)出nonce為9的交易,在其被打包前,又構(gòu)造一筆nonce為9的交易發(fā)出,這兩筆交易各自在ETH網(wǎng)絡(luò)上廣播,節(jié)點(diǎn)同時(shí)收到nonce值相同的未確認(rèn)交易,這時(shí)產(chǎn)生了沖突,節(jié)點(diǎn)默認(rèn)會(huì)比較兩筆交易的Gas Price,將Gas Price相對(duì)較高的交易放入交易池,較低那筆則會(huì)被舍棄并不再?gòu)V播。

txpool.jpg

如圖所示,在由地址A發(fā)出的且已經(jīng)被打包進(jìn)區(qū)塊的交易已有10筆,nonce的值為9(從0開(kāi)始)。所以交易池中nonce為10,11,12的交易接下來(lái)都有可能按序被礦工打包,這些未確認(rèn)交易處于pending狀態(tài)。但因?yàn)槿鄙賜once為13的交易,那nonce為14的交易暫時(shí)不可能被礦工列入打包的候選交易,處于queued狀態(tài)。待nonce為13的交易出現(xiàn)在交易池中時(shí),nonce為14的這筆交易才有可能被打包。

所以從某個(gè)地址批量發(fā)出交易時(shí),pending隊(duì)列里nonce值靠前的某筆交易的Gas Price過(guò)低,遲遲不能被礦工打包,那么nonce在其后的未確認(rèn)交易都不可能被打包。如果不去解決前面被擁堵的交易,還持續(xù)不間斷地從該地址發(fā)出交易,哪怕后面這些交易給出了特別高的Gas Price,也不能促使礦工去打包。這么做往交易池的pending隊(duì)列里塞入了更多的交易,反而會(huì)使ETH愈加擁堵。

擁堵時(shí)可以采取的策略

ETH網(wǎng)絡(luò)出現(xiàn)擁堵時(shí),我們也不可能因此暫停掉業(yè)務(wù)。那么從某地址批量發(fā)出交易時(shí),不要無(wú)節(jié)制地往pending隊(duì)列塞入交易。通常我們會(huì)設(shè)置一個(gè)水位,當(dāng)某個(gè)地址在交易池的pending隊(duì)列中有50筆未確認(rèn)交易時(shí),就應(yīng)該暫時(shí)停止用該地址發(fā)送交易,然后等待這50筆未確認(rèn)交易被礦工打包消化光后,再重新開(kāi)啟。另外執(zhí)行批量操作時(shí),可以使用多個(gè)熱錢(qián)包地址去發(fā)出交易,避免單個(gè)地址被其某筆交易堵塞而導(dǎo)致整個(gè)功能模塊暫停。還有,如果某筆未確認(rèn)的交易阻塞已久,重新構(gòu)建這筆交易并設(shè)置更高更合理的Gas Price然后發(fā)出,礦工節(jié)點(diǎn)會(huì)用這筆新的交易替換掉原先被阻塞的,往往能解決隊(duì)列擁堵的問(wèn)題,這種動(dòng)態(tài)補(bǔ)償機(jī)制也為諸多交易所,錢(qián)包所使用。

上面的方案是為了在ETH擁堵時(shí)保證應(yīng)用的正常運(yùn)行和用戶體驗(yàn),本質(zhì)上提高了Gas Price,加大了手續(xù)費(fèi)的消耗。Gas Price由市場(chǎng)決定,我們并沒(méi)有定價(jià)權(quán),那在Gas Price無(wú)法節(jié)省的前提下,批量發(fā)送交易時(shí)所需要的Used Gas能否減少呢?如果能有什么辦法減少單筆交易的Gas消耗,也就變相降低了手續(xù)費(fèi)。

合約層面的批量轉(zhuǎn)賬

假設(shè)從一個(gè)地址往10個(gè)地址發(fā)送某ERC20 Token,目前我們是針對(duì)每個(gè)地址去創(chuàng)建一筆transfer交易,批量產(chǎn)生了10筆交易然后發(fā)出。那有沒(méi)有什么辦法能像BTC那樣,在一筆交易里寫(xiě)入多筆轉(zhuǎn)賬操作呢,興許這樣能降低總的Gas消耗?

一些Token在合約里就實(shí)現(xiàn)了批量轉(zhuǎn)賬的batchTransfer方法,該方法接收數(shù)組,然后開(kāi)始循環(huán),針對(duì)數(shù)組中的每個(gè)地址進(jìn)行發(fā)起一筆轉(zhuǎn)賬。讓我們看一下BEC對(duì)batchTransfer的實(shí)現(xiàn):

becbug.png

邏輯上是OK的,但很遺憾,BEC的開(kāi)發(fā)團(tuán)隊(duì)在這里出了個(gè)極其嚴(yán)重的Bug:*運(yùn)算后,沒(méi)有對(duì)結(jié)果進(jìn)行溢出判斷,存在嚴(yán)重風(fēng)險(xiǎn),可為攻擊者利用來(lái)憑空生成代幣。batchTransfer也并不是ERC20代幣的標(biāo)準(zhǔn),某些Token實(shí)現(xiàn)了這個(gè)方法,某些Token本著越簡(jiǎn)單越安全的原則,沒(méi)有在Token的合約里去實(shí)現(xiàn)類(lèi)似的批量轉(zhuǎn)賬功能。如果你要在Token合約內(nèi)實(shí)現(xiàn),請(qǐng)務(wù)必要做好代碼的安全審計(jì)。

作為第三方應(yīng)用開(kāi)發(fā),Token的合約不是我們能決定的,絕大多數(shù)的Token也沒(méi)有在合約內(nèi)實(shí)現(xiàn)batchTransfer功能,我們還能通過(guò)其他的方式在合約層面實(shí)現(xiàn)批量轉(zhuǎn)賬嗎?

首先,ETH是無(wú)法在一筆交易里直接去多次調(diào)用合約或者同時(shí)調(diào)用多個(gè)合約的,但是,合約內(nèi)的函數(shù)是可以外部調(diào)用其他的合約的。我們可以將批量轉(zhuǎn)賬的邏輯以一個(gè)智能合約來(lái)實(shí)現(xiàn),通過(guò)該合約的去多次外部調(diào)用Token合約內(nèi)transfer方法,這樣我們的目的也就達(dá)到了,原理如下圖所示:

call.jpg

地址B上是一個(gè)支持ERC20標(biāo)準(zhǔn)的代幣合約,提供有transfer方法,我們可以再部署一個(gè)中間合約來(lái)實(shí)現(xiàn)批量調(diào)用的邏輯,中間合約里的batchTranfer接收ERC20代幣的合約地址,代幣接收方地址的數(shù)組,與之相對(duì)的token轉(zhuǎn)賬數(shù)量數(shù)組。在該方法對(duì)接受方數(shù)組里的每個(gè)接受者,外部調(diào)用Token合約(address B)上的transfer函數(shù), 實(shí)現(xiàn)代碼:_token.call(bytes4(keccak256("transfer(address,uint256)"), receivers[index], amounts[index])。這里使用到了call,這是solidity中很底層的一個(gè)方法,使用不當(dāng)會(huì)出現(xiàn)安全隱患,請(qǐng)先去足夠了解有關(guān)call的更多細(xì)節(jié)!

需要注意的是,在中間合約(address A)外部調(diào)用Token合約(address B)的transfer時(shí),由于call的特性,msg.sender會(huì)從起始的address C 變成 address A,其實(shí)是從address A中轉(zhuǎn)出Token,那就要先往address A中轉(zhuǎn)入足夠的Token。為了保障中間合約(address A)上Token的安全,合約必須要設(shè)置一個(gè)owner,batchTransfer內(nèi)一定要有檢查address C調(diào)用者是否與owner一致的邏輯。owner為address C, 只有address C能調(diào)用這個(gè)中間合約,且整個(gè)交易的手續(xù)費(fèi)從address C扣除。

實(shí)現(xiàn)時(shí)還需要具體考慮安全和性能的問(wèn)題,比如_receivers數(shù)組和_amounts數(shù)組的長(zhǎng)度是否相等,循環(huán)中call調(diào)用失敗后應(yīng)該回退整個(gè)批量交易,避免傳入的數(shù)組長(zhǎng)度過(guò)大等等。

來(lái)看看該方案實(shí)際的效果:

下圖是從某地址去調(diào)用Token合約內(nèi)transfer方法的消耗:

下圖是我們上面實(shí)現(xiàn)的,通過(guò)BatchTransfer Caller這個(gè)中間合約去調(diào)用Token合約的transfer,但這里我們先只往參數(shù)數(shù)組里傳入1個(gè)接收者:

可以看到因?yàn)槎嗔艘恍┲虚g操作,參數(shù)結(jié)構(gòu)也更加復(fù)雜,當(dāng)傳入數(shù)組長(zhǎng)度為1時(shí),這樣比直接調(diào)用Token的transfer的Gas的消耗反而增加了一些。那我們?cè)倏纯赐鶖?shù)組里塞入10個(gè)接受者時(shí)的情況:

此時(shí),與直接去調(diào)用Token的transfer方法10次的消耗量相比,通過(guò)BatchTransfer Caller合約來(lái)實(shí)現(xiàn)批量轉(zhuǎn)賬節(jié)省了大概30%的Gas總消耗,是不是非??捎^呢。

總結(jié)

在ETH網(wǎng)絡(luò)擁堵時(shí),如果要批量發(fā)起交易,首先要考慮操作的安全問(wèn)題,在交易確認(rèn)速度和Gas Price中尋找一個(gè)平衡點(diǎn),然后通過(guò)一些動(dòng)態(tài)的調(diào)整機(jī)制以及合約的外部調(diào)用來(lái)保障交易的確認(rèn)速度以及減少

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容