以太坊手續費之殤
以太坊本質上是一個虛擬機,交易的gas成本主要由虛擬機的運行成本組成,當你發送 Token或者執行智能合約時,以太坊在處理這筆交易的程序中需要進行計算,會根據復雜度消耗一筆gas費(ETH),
目前以太坊上的大部分應用都是以智能合約的形態在運行,而以太坊智能合約采用solidity語言撰寫,因而在撰寫智能合約時,不僅要考慮安全,還要考慮語言的優化,以便合約應用更高效便宜,一方面也為自己的用戶節省應用手續費,
如何節省手續費
設計合約
因為合約執行的復雜度和計算量直接與執行消耗的gas相關,所以在合約設計,創建合約時,最好對需要上鏈的資料結構進行優化設計,只將必要的資料上鏈存盤,比如基于ERC-721的NFT標準,NFT合約只要求將NFT的唯一TokenId上鏈存盤,TokenId與對應標記物品的關系可以用URI的方式指向傳統互聯網網路上的一個資源;
比如,我們設計一個基于魚類的NFT合約,按照ERC-721標準,我們只需要實作類似如下的合約即可
Contract MyFish {
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// .......
function tokenURI(uint256 tokenId) public view returns (string memory) {
// ......
}
//......
}
以上合約中,我們只將NFT唯一TokenId在以太坊上,并通過URI標記對應物品
假如我們將魚類的其它屬性也上鏈存盤,合約設計成如下形式
Contract MyFish {
struct Fish {
uint length;
uint weight;
uint age;
}
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping from token ID to attributes
mapping(uint256 => Fish) private _fishes;
// .......
function mintFish(uint256 tokenId, uint _length, uint _weight, uint _age) public returns (uint256) {
// ......
Fish memory fish = Fish(
_length,
_weight,
_age
)
// ......
}
function tokenAttr(uint256 tokenId) public view returns (uint _length, uint _weight, uint _age) {
// ......
}
//......
}
按如上合約的方式,我們將每條魚的更多屬性也上鏈存盤,雖然這樣更符合區塊鏈去中心化的目的之一,保證資料不可更改,但是這樣會導致每次鑄造NFT時要在以太坊上存盤更多內容,以及涉及更多轉移、計算流程等時合約執行復雜的更高,必然帶來更多的手續費消耗,
雖然上面的方案一存在一定的中心化風險,實際上這個問題也是可以有更多其它方案來解決的;比如我們將更多的屬性類的資料放到其它手續費更低的區塊鏈系統中,比如我們可以存盤到Filecoin鏈上,越來越多的NFT專案都在采用這類方案;
以及目前發展越來越完善的以太坊Layer 2擴容,一方面的目的也是解決這類問題,將部分資料的存盤遷移到專門的存盤模塊上,以降低以太坊操作的手續費,
避免將以太坊合約用作資料存盤,
Opensea中的NFT拍賣合約,NFT在上架拍賣時,并沒有將該NFT的拍賣資訊上鏈,在拍賣完成或者下架時,才將該次拍賣的的hash資料上鏈,確保不會在合約中操作無效的拍賣訂單,這樣也是一種降低存盤、降低手續費的方案,
存盤
上面的合約設計中提到了避免直接將以太坊區塊鏈當作存盤平臺,除此之外,在合約代碼執行中,也有一些技巧避免過度的手續費消耗,
比如,如下的兩端合約代碼
uint256 public count;
// ...
for (uint256 i = 0; i < 10; ++i) {
// ...
++count;
}
更節省手續費的寫法
for (uint256 i = 0; i < 10; ++i) {
// ...
}
count += 10;
第二種方式組織的代碼,可以避免頻繁的修改合約中的存盤塊變數,降低手續費,
避免重復寫入,最好一次在最后盡可能多地寫入到存盤變數,
變數排序對gas的影響
由于EVM操作都是以32位元組為單位執行的,因此編譯器將嘗試將變數打包成32位元組集進行訪問,以減少訪問時間, 但是,編譯器不夠智能,無法自動優化變數分組,它將靜態大小的變數分組為32個位元組的組,例如:
contract MyContract {
uint64 public a;
uint64 public b;
uint64 public c;
uint64 public d;
function test() {
a = 1;
b = 2;
c = 3;
d = 4;
}
}
執行test()時,看起來已經存盤了四個變數,由于這四個變數之和恰好是32個位元組,因此實際執行了一個SSTORE,這只需要20,000 gas,
再看下一個例子:
contract MyContract {
uint64 public a;
uint64 public b;
byte e;
uint64 public c;
uint64 public d;
function test() {
a = 1;
b = 2;
c = 3;
d = 4;
}
}
中間插入了另一個變數,結果造成a,b,e和c會被分為一組,d獨立為一組,同樣的test()造成兩次寫入,消耗40000 gas,
最后再看一個例子:
contract MyContract {
uint64 public a;
uint64 public b;
uint64 public c;
uint64 public d;
function test() {
a = 1;
b = 2;
// ... do something
c = 3;
d = 4;
}
}
這與第一個例子的區別在于,在存盤a和b之后,完成了其他事情,最后存盤了c和d,結果這次將導致兩次寫入,因為當執行“執行某事”時,編譯器確定打包操作已結束,然后發送寫入,但是,由于第二次寫入是同一組資料,因此認為它是被修改的,將消耗總共25,000個氣體,
建議: 根據上述原則,我們可以很容易地知道如何處理它,
正確的排序和分組 將資料大小分組為32個位元組,并將通常同時更新的變數放在一起, 不好的代碼例子:
contract MyContract {
uint128 public hp;
uint128 public maxHp;
uint32 level;
uint128 public mp;
uint128 public maxMp;
}
好的例子:
contract MyContract {
uint128 public hp;
uint128 public mp;
uint128 public maxHp;
uint128 public maxMp;
uint32 level;
}
這里我們假設hp和mp更頻繁地更新,并且maxHp和maxMp更頻繁地一起更新,
盡量一次訪問 不好的代碼例子:
function test() {
hp = 1;
// ... do something
mp = 2;
}
好的例子:
function test() {
// ... do something
hp = 1;
mp = 2;
}
轉賬
Call, send 和transfer 函式對應于CALL指令,基本消耗是7,400 gas,事實上,消費將近7,600 gas,值得注意的是,如果轉賬到一個從未見過的地址,將額外增加25,000個gas,
// 沒有額外的消耗樣例
function withdraw(uint256 amount){
msg.sender.transfer(amount);
}
// 可能會有額外的消耗樣例(receiver引數未被使用,多余引數)
function withdrawTo(uint256 amount, address receiver) {
receiver.transfer(amount);
}
呼叫合約函式的成本優化
當呼叫合約額的功能時,為了執行功能,它需要gas,因此,優化使用較少gas的功能非常重要,在考慮每個合約時時,可以采用多種不同的方式,這里有一些可能在執行程序中節省gas的方法,
減少昂貴的操作
昂貴的操作是指一些需要更多gas值的操作碼,例如SSTORE,以下是一些減少昂貴操作的方法,
使用短路規則
運算子 || 和&&適用常見的短路規則,這意味著在運算式f(x)|| g(y)中,如果f(x)的計算結果為真,即使它有副作用,也不會評估g(y),
因此,如果邏輯操作包括昂貴的操作和低成本操作,那么以昂貴的操作可以短路的方式安排將在一些執行中減少gas,
如果f(x)是便宜的并且g(y)是昂貴的,邏輯運算代碼(便宜的放在前面):
OR : f(x) || g(y)
AND: f(x) && g(y)
如果短路,將節省更多的氣體,
f(x)與g(y)安排AND操作相比,如果回傳錯誤的概率要高得多,f(x) && g(y)可能會導致通過短路節省更多的氣體,
f(x)與g(y)安排OR運算相比,如果回傳真值的概率要高得多,f(x) || g(y)可能會導致通過短路節省更多氣體,
洗掉無用的代碼可以在執行時節省gas
洗掉無用的代碼即使在執行函式時也會節省gas,
在實作簡單功能時不使用第三方庫對于簡單的應用場景來說更便宜
呼叫第三方庫以獲得簡單的用法可能代價高昂,
因為引入庫中的部分功能可能是不必要的,這些會顯著增加合約的部署成本,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/291380.html
標籤:區塊鏈
