Çağrı Verisi Optimizasyonu için Kısa ABI'ler
Giriş
Bu makalede iyimser toplamalar, bunların işlem maliyetleri ve bu farklı maliyet yapısının Ethereum Ana Ağı'ndakinden farklı şeyler için optimizasyon yapmamızı nasıl gerektirdiği hakkında bilgi edineceksiniz. Ayrıca bu optimizasyonu nasıl uygulayacağınızı da öğreneceksiniz.
Tam beyan
Ben tam zamanlı bir Optimismopens in a new tab çalışanıyım, bu nedenle bu makaledeki örnekler Optimism üzerinde çalışacaktır. Ancak, burada açıklanan teknik diğer toplamalar için de aynı şekilde çalışmalıdır.
Terminoloji
Toplamalar tartışılırken, üretim Ethereum ağı olan Ana Ağ için 'katman 1' (L1) terimi kullanılır. 'Katman 2' (L2) terimi, güvenlik için L1'e dayanan ancak işlemlerinin çoğunu zincir dışında yapan toplama veya diğer herhangi bir sistem için kullanılır.
L2 işlemlerinin maliyetini nasıl daha da düşürebiliriz?
İyimser toplamalar, herkesin bunları inceleyebilmesi ve mevcut durumun doğru olduğunu doğrulayabilmesi için her geçmiş işlemin kaydını tutmak zorundadır. Verileri Ethereum Ana Ağı'na almanın en ucuz yolu, onları çağrı verisi olarak yazmaktır. Bu çözüm hem Optimismopens in a new tab hem de Arbitrumopens in a new tab tarafından seçilmiştir.
L2 işlemlerinin maliyeti
L2 işlemlerinin maliyeti iki bileşenden oluşur:
- Genellikle son derece ucuz olan L2 işleme
- Ana Ağ gaz maliyetlerine bağlı olan L1 depolaması
Bu yazıyı yazarken Optimism'de L2 gaz maliyeti 0,001 Gwei'dir. Öte yandan L1 gazının maliyeti ise yaklaşık 40 gwei'dir. Mevcut fiyatları buradan görebilirsinizopens in a new tab.
Bir çağrı verisi baytının maliyeti, sıfır ise 4 gaz, başka bir değer ise 16 gazdır. EVM'deki en pahalı işlemlerden biri depolamaya yazmaktır. 32 baytlık bir kelimeyi L2'de depolamaya yazmanın maksimum maliyeti 22.100 gazdır. Şu anda bu, 22,1 gwei'dir. Dolayısıyla, çağrı verisinden yalnızca sıfır değerli tek bir bayt tasarruf edebilirsek, depolamaya yaklaşık 200 bayt yazabilir ve yine de kârlı çıkabiliriz.
ABI
İşlemlerin büyük bir çoğunluğu, bir sözleşmeye dıştan sahiplenilmiş bir hesaptan erişir. Çoğu sözleşme Solidity ile yazılmıştır ve veri alanlarını uygulama ikili arayüzüne (ABI)opens in a new tab göre yorumlar.
Ancak ABI, bir çağrı verisi baytının maliyetinin yaklaşık olarak dört aritmetik işlemle aynı olduğu L1 için tasarlanmıştır, bir çağrı verisi baytının bin aritmetik işlemden daha pahalı olduğu L2 için değil. Çağrı verisi şu şekilde bölünür:
| Bölüm | Uzunluk | Bayt | İsraf edilen bayt | İsraf edilen gaz | Gerekli bayt | Gerekli gaz |
|---|---|---|---|---|---|---|
| Fonksiyon seçici | 4 | 0-3 | 3 | 48 | 1 | 16 |
| Sıfırlar | 12 | 4-15 | 12 | 48 | 0 | 0 |
| Hedef adresi | 20 | 16-35 | 0 | 0 | 20 | 320 |
| Miktar | 32 | 36-67 | 17 | 64 | 15 | 240 |
| Toplam | 68 | 160 | 576 |
Açıklama:
- Fonksiyon seçici: Sözleşmenin 256'dan az fonksiyonu olduğundan, bunları tek bir baytla ayırt edebiliriz. Bu baytlar genellikle sıfır değildir ve bu nedenle maliyeti 16 gazdıropens in a new tab.
- Sıfırlar: Bu baytlar her zaman sıfırdır çünkü yirmi baytlık bir adres, onu tutmak için otuz iki baytlık bir kelime gerektirmez.
Sıfır değeri taşıyan baytların maliyeti dört gazdır (sarı bültene bakınopens in a new tab, Ek G,
s. 27,
Gtxdatazerodeğeri için). - Miktar: Bu sözleşmede
decimalsdeğerinin on sekiz (normal değer) olduğunu ve transfer edeceğimiz maksimum jeton miktarının 1018 olacağını varsayarsak, maksimum 1036 tutarında bir miktar elde ederiz. 25615 > 1036, yani on beş bayt yeterlidir.
L1'de 160 gazlık bir israf normalde ihmal edilebilir düzeydedir. Bir işlemin maliyeti en az 21.000 gazdıropens in a new tab, bu nedenle fazladan %0,8'lik bir oran önemli değildir.
Ancak L2'de işler farklıdır. İşlem maliyetinin neredeyse tamamı, işlemi L1'e yazmaktır.
İşlem çağrı verisine ek olarak 109 baytlık bir işlem başlığı (hedef adresi, imza vb.) bulunur.
Bu nedenle toplam maliyet 109*16+576+160=2480 olup bunun yaklaşık %6,5'ini boşa harcıyoruz.
Hedef sözleşmeyi kontrol etmediğinizde maliyetleri düşürme
Hedef sözleşme üzerinde kontrolünüz olmadığını varsayarsak, yine de bunaopens in a new tab benzer bir çözüm kullanabilirsiniz. İlgili dosyalara bir göz atalım.
Token.sol
Bu, hedef sözleşmediropens in a new tab.
Bu, bir ek özelliğe sahip standart bir ERC-20 sözleşmesidir.
Bu faucet fonksiyonu, herhangi bir kullanıcının kullanmak üzere bir miktar jeton almasını sağlar.
Bu, bir üretim ERC-20 sözleşmesini kullanışsız hale getirse de bir ERC-20 yalnızca testi kolaylaştırmak için mevcut olduğunda işleri kolaylaştırır.
1 /**2 * @dev Çağırana oynaması için 1000 jeton verir3 */4 function faucet() external {5 _mint(msg.sender, 1000);6 } // function faucetCalldataInterpreter.sol
Bu, işlemlerin daha kısa çağrı verisi ile çağırması gereken sözleşmediropens in a new tab. Satır satır üzerinden geçelim.
1//SPDX-License-Identifier: Unlicense2pragma solidity ^0.8.0;345import { OrisUselessToken } from "./Token.sol";Onu nasıl çağıracağımızı bilmek için jeton fonksiyonuna ihtiyacımız var.
1contract CalldataInterpreter {23 OrisUselessToken public immutable token;Vekil olduğumuz jetonun adresi.
12 /**3 * @dev Jeton adresini belirtin4 * @param tokenAddr_ ERC-20 sözleşme adresi5 */6 constructor(7 address tokenAddr_8 ) {9 token = OrisUselessToken(tokenAddr_);10 } // constructorTümünü gösterJeton adresi, belirtmemiz gereken tek parametredir.
1 function calldataVal(uint startByte, uint length)2 private pure returns (uint) {Çağrı verisinden bir değer okuyun.
1 uint _retVal;23 require(length < 0x21,4 "calldataVal uzunluk sınırı 32 bayttır");56 require(length + startByte <= msg.data.length,7 "calldataVal, calldatasize ötesini okumaya çalışıyor");Belleğe tek bir 32 baytlık (256 bit) kelime yükleyeceğiz ve istediğimiz alanın parçası olmayan baytları kaldıracağız. Bu algoritma 32 bayttan uzun değerler için çalışmaz ve elbette çağrı verisinin sonundan ötesini okuyamayız. L1'de gazdan tasarruf etmek için bu testleri atlamak gerekebilir, ancak L2'de gaz son derece ucuzdur, bu da aklımıza gelebilecek her türlü mantık kontrolünü yapmamızı sağlar.
1 assembly {2 _retVal := calldataload(startByte)3 }Verileri fallback() çağrısından (aşağıya bakın) kopyalayabilirdik, ancak EVM'nin assembly dili olan Yulopens in a new tab dilini kullanmak daha kolaydır.
Burada startByte ile startByte+31 arasındaki baytları yığına okumak için CALLDATALOAD işlem kodunuopens in a new tab kullanıyoruz.
Genel olarak, Yul'daki bir işlem kodunun sözdizimi <işlem kodu adı>(<varsa ilk yığın değeri>,<varsa ikinci yığın değeri>...) şeklindedir.
12 _retVal = _retVal >> (256-length*8);Alanın yalnızca en anlamlı uzunluktaki baytları parçasıdır, bu nedenle diğer değerlerden kurtulmak için sağa kaydırmaopens in a new tab yaparız.
Bu, değeri alanın sağına taşıma gibi ek bir avantaja sahiptir, dolayısıyla değerin kendisidir, değer çarpı 256bir şey değildir.
12 return _retVal;3 }456 fallback() external {Bir Solidity sözleşmesine yapılan bir çağrı, işlev imzalarından herhangi biriyle eşleşmediğinde, fallback() işleviniopens in a new tab çağırır (varsa).
CalldataInterpreter durumunda, başka external veya public işlevler olmadığı için herhangi bir çağrı buraya gelir.
1 uint _func;23 _func = calldataVal(0, 1);Bize işlevi söyleyen çağrı verisinin ilk baytını okuyun. Bir işlevin burada mevcut olmamasının iki nedeni vardır:
pureveyaviewolan işlevler durumu değiştirmez ve gaza mal olmaz (zincir dışı çağrıldığında). Gaz maliyetlerini düşürmeye çalışmanın bir anlamı yoktur.msg.senderopens in a new tab'a dayanan işlevler.msg.senderdeğeri, arayanın değilCalldataInterpreteradresinin adresi olacaktır.
Ne yazık ki, ERC-20 belirtimlerine bakıldığındaopens in a new tab, bu geriye yalnızca bir işlev bırakır: transfer.
Bu bize sadece iki işlev bırakıyor: transfer (transferFrom çağırabildiğimiz için) ve faucet (jetonları bizi çağıran kişiye geri aktarabildiğimiz için).
12 // Çağrı verisindeki bilgileri kullanarak jetonun durum değiştirme3 // yöntemlerini çağırın45 // faucet6 if (_func == 1) {Parametresi olmayan faucet() çağrısı.
1 token.faucet();2 token.transfer(msg.sender,3 token.balanceOf(address(this)));4 }token.faucet() öğesini çağırdıktan sonra jetonları alırız. Ancak, vekil sözleşmesi olarak jetonlara ihtiyacımız yok.
Bizi arayan EOA (harici olarak sahip olunan hesap) veya sözleşmenin ihtiyacı var.
Bu yüzden tüm jetonlarımızı bizi arayan kişiye aktarıyoruz.
1 // transfer (bunun için bir yetkimiz olduğunu varsayalım)2 if (_func == 2) {Jeton transferi iki parametre gerektirir: hedef adres ve miktar.
1 token.transferFrom(2 msg.sender,Arayanların yalnızca sahip oldukları jetonları transfer etmelerine izin veriyoruz
1 address(uint160(calldataVal(1, 20))),Hedef adresi 1 numaralı bayttan başlar (0 numaralı bayt işlevdir). Bir adres olarak 20 bayt uzunluğundadır.
1 calldataVal(21, 2)Bu özel sözleşme için, herhangi birinin transfer etmek isteyeceği maksimum jeton sayısının iki bayta (65536'dan az) sığdığını varsayıyoruz.
1 );2 }Genel olarak, bir transfer 35 bayt çağrı verisi alır:
| Bölüm | Uzunluk | Bayt |
|---|---|---|
| Fonksiyon seçici | 1 | 0 |
| Hedef adresi | 32 | 1-32 |
| Miktar | 2 | 33-34 |
1 } // fallback23} // contract CalldataInterpretertest.js
Bu JavaScript birim testiopens in a new tab bize bu mekanizmanın nasıl kullanılacağını (ve doğru çalışıp çalışmadığını nasıl doğrulayacağımızı) gösterir. chaiopens in a new tab ve ethersopens in a new tab'ı anladığınızı varsayacağım ve yalnızca sözleşmeye özel olarak uygulanan kısımları açıklayacağım.
1const { expect } = require("chai");23describe("CalldataInterpreter", function () {4 it("Jetonları kullanmamıza izin vermeli", async function () {5 const Token = await ethers.getContractFactory("OrisUselessToken")6 const token = await Token.deploy()7 await token.deployed()8 console.log("Jeton adresi:", token.address)910 const Cdi = await ethers.getContractFactory("CalldataInterpreter")11 const cdi = await Cdi.deploy(token.address)12 await cdi.deployed()13 console.log("CalldataInterpreter adresi:", cdi.address)1415 const signer = await ethers.getSigner()Tümünü gösterHer iki sözleşmeyi de dağıtarak başlıyoruz.
1 // Oynamak için jetonları al2 const faucetTx = {ABI'yi takip etmediğimiz için, normalde kullanacağımız üst düzey işlevleri (token.faucet() gibi) işlem oluşturmak için kullanamayız.
Bunun yerine, işlemi kendimiz oluşturmalı ve sonra göndermeliyiz.
1 to: cdi.address,2 data: "0x01"İşlem için sağlamamız gereken iki parametre vardır:
to, hedef adres. Bu, çağrı verisi yorumlayıcı sözleşmesidir.data, gönderilecek çağrı verisi. Bir musluk çağrısı durumunda, veri tek bir bayttır,0x01.
12 }3 await (await signer.sendTransaction(faucetTx)).wait()İmzalayanın sendTransaction yöntemini çağırırız çünkü hedefi zaten belirttik (faucetTx.to) ve işlemin imzalanması gerekiyor.
1// Musluğun jetonları doğru bir şekilde sağladığını kontrol edin2expect(await token.balanceOf(signer.address)).to.equal(1000)Burada bakiyeyi doğruluyoruz.
view işlevlerinde gaz tasarrufu yapmaya gerek yoktur, bu yüzden onları normal bir şekilde çalıştırırız.
1// CDI'ye bir yetki verin (onaylar vekil aracılığıyla yapılamaz)2const approveTX = await token.approve(cdi.address, 10000)3await approveTX.wait()4expect(await token.allowance(signer.address, cdi.address)).to.equal(10000)Transfer yapabilmesi için çağrı verisi yorumlayıcısına bir yetki verin.
1// Jetonları transfer et2const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"3const transferTx = {4 to: cdi.address,5 data: "0x02" + destAddr.slice(2, 42) + "0100",6}Bir transfer işlemi oluşturun. İlk bayt "0x02"dir, bunu hedef adresi ve son olarak miktar (0x0100, ondalık sistemde 256'dır) takip eder.
1 await (await signer.sendTransaction(transferTx)).wait()23 // 256 jeton daha azımız olduğunu kontrol edin4 expect (await token.balanceOf(signer.address)).to.equal(1000-256)56 // Ve hedefimizin onları aldığını7 expect (await token.balanceOf(destAddr)).to.equal(256)8 }) // it9}) // describeTümünü gösterHedef sözleşmeyi kontrol ettiğinizde maliyeti düşürme
Hedef sözleşme üzerinde kontrolünüz varsa, çağrı verisi yorumlayıcısına güvendikleri için msg.sender kontrollerini atlayan işlevler oluşturabilirsiniz.
Bunun nasıl çalıştığına dair bir örneği control-contract dalında buradan görebilirsinizopens in a new tab.
Sözleşme yalnızca harici işlemlere yanıt veriyorsa, yalnızca bir sözleşmeye sahip olarak idare edebilirdik. Ancak bu, birleştirilebilirliği bozar. Normal ERC-20 çağrılarına yanıt veren bir sözleşmeye ve kısa çağrı verili işlemlere yanıt veren başka bir sözleşmeye sahip olmak çok daha iyidir.
Token.sol
Bu örnekte Token.sol dosyasını değiştirebiliriz.
Bu, yalnızca vekilin çağırabileceği bir dizi işleve sahip olmamızı sağlar.
İşte yeni kısımlar:
1 // CalldataInterpreter adresini belirtmesine izin verilen tek adres2 address owner;34 // CalldataInterpreter adresi5 address proxy = address(0);ERC-20 sözleşmesinin yetkili vekilin kimliğini bilmesi gerekir. Ancak, değeri henüz bilmediğimiz için bu değişkeni kurucuda ayarlayamayız. Bu sözleşme ilk olarak örneklenir çünkü vekil, kurucusunda jetonun adresini bekler.
1 /**2 * @dev ERC20 kurucusunu çağırır.3 */4 constructor(5 ) ERC20("Oris'in işe yaramaz jetonu-2", "OUT-2") {6 owner = msg.sender;7 }Oluşturanın adresi (owner olarak adlandırılır) burada saklanır çünkü vekili ayarlamasına izin verilen tek adres budur.
1 /**2 * @dev Vekil için adresi ayarla (CalldataInterpreter).3 * Yalnızca sahip tarafından bir kez çağrılabilir4 */5 function setProxy(address _proxy) external {6 require(msg.sender == owner, "Yalnızca sahip tarafından çağrılabilir");7 require(proxy == address(0), "Vekil zaten ayarlanmış");89 proxy = _proxy;10 } // function setProxyTümünü gösterVekil, güvenlik kontrollerini atlayabildiği için ayrıcalıklı erişime sahiptir.
Vekile güvenebileceğimizden emin olmak için, bu işlevi yalnızca ownerın ve yalnızca bir kez çağırmasına izin veriyoruz.
proxy gerçek bir değere (sıfır değil) sahip olduğunda, bu değer değiştirilemez, bu nedenle sahibi dolandırıcı olmaya karar verse veya anımsatıcısı ortaya çıksa bile yine de güvendeyiz.
1 /**2 * @dev Bazı işlevler yalnızca vekil tarafından çağrılabilir.3 */4 modifier onlyProxy {Bu bir değiştirici işlevidiropens in a new tab, diğer işlevlerin çalışma şeklini değiştirir.
1 require(msg.sender == proxy);İlk olarak, vekil tarafından arandığımızı ve başka kimsenin aramadığını doğrulayın.
Değilse, revert.
1 _;2 }Eğer öyleyse, değiştirdiğimiz işlevi çalıştırın.
1 /* Vekilin hesaplar için gerçekten vekillik yapmasına izin veren işlevler */23 function transferProxy(address from, address to, uint256 amount)4 public virtual onlyProxy() returns (bool)5 {6 _transfer(from, to, amount);7 return true;8 }910 function approveProxy(address from, address spender, uint256 amount)11 public virtual onlyProxy() returns (bool)12 {13 _approve(from, spender, amount);14 return true;15 }1617 function transferFromProxy(18 address spender,19 address from,20 address to,21 uint256 amount22 ) public virtual onlyProxy() returns (bool)23 {24 _spendAllowance(from, spender, amount);25 _transfer(from, to, amount);26 return true;27 }Tümünü gösterBunlar normalde mesajın doğrudan jetonları transfer eden veya bir yetkiyi onaylayan kuruluştan gelmesini gerektiren üç işlemdir. Burada bu işlemlerin şu nitelikleri taşıyan vekil versiyonları mevcuttur:
onlyProxy()tarafından değiştirilmiştir, böylece başka kimsenin onları kontrol etmesine izin verilmez.- Normalde
msg.senderolacak adresi ekstra bir parametre olarak alır.
CalldataInterpreter.sol
Çağrı verisi yorumlayıcısı, vekil işlevlerin bir msg.sender parametresi alması ve transfer için bir yetkiye gerek olmaması dışında, yukarıdakiyle neredeyse aynıdır.
1 // transfer (yetkiye gerek yok)2 if (_func == 2) {3 token.transferProxy(4 msg.sender,5 address(uint160(calldataVal(1, 20))),6 calldataVal(21, 2)7 );8 }910 // approve11 if (_func == 3) {12 token.approveProxy(13 msg.sender,14 address(uint160(calldataVal(1, 20))),15 calldataVal(21, 2)16 );17 }1819 // transferFrom20 if (_func == 4) {21 token.transferFromProxy(22 msg.sender,23 address(uint160(calldataVal( 1, 20))),24 address(uint160(calldataVal(21, 20))),25 calldataVal(41, 2)26 );27 }Tümünü gösterTest.js
Önceki test kodu ile bu kod arasında birkaç değişiklik var.
1const Cdi = await ethers.getContractFactory("CalldataInterpreter")2const cdi = await Cdi.deploy(token.address)3await cdi.deployed()4await token.setProxy(cdi.address)ERC-20 sözleşmesine hangi vekile güveneceğini söylememiz gerekiyor
1console.log("CalldataInterpreter adresi:", cdi.address)23// Yetkileri doğrulamak için iki imzalayan gerekir4const signers = await ethers.getSigners()5const signer = signers[0]6const poorSigner = signers[1]approve() ve transferFrom() kontrol etmek için ikinci bir imzalayana ihtiyacımız var.
Buna poorSigner diyoruz çünkü jetonlarımızdan hiçbirini almıyor (tabii ki ETH'ye sahip olması gerekiyor).
1// Jetonları transfer et2const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"3const transferTx = {4 to: cdi.address,5 data: "0x02" + destAddr.slice(2, 42) + "0100",6}7await (await signer.sendTransaction(transferTx)).wait()ERC-20 sözleşmesi vekile (cdi) güvendiği için, transferleri iletmek için bir yetkiye ihtiyacımız yok.
1// onay ve transferFrom2const approveTx = {3 to: cdi.address,4 data: "0x03" + poorSigner.address.slice(2, 42) + "00FF",5}6await (await signer.sendTransaction(approveTx)).wait()78const destAddr2 = "0xE1165C689C0c3e9642cA7606F5287e708d846206"910const transferFromTx = {11 to: cdi.address,12 data: "0x04" + signer.address.slice(2, 42) + destAddr2.slice(2, 42) + "00FF",13}14await (await poorSigner.sendTransaction(transferFromTx)).wait()1516// Onay / transferFrom kombinasyonunun doğru yapıldığını kontrol edin17expect(await token.balanceOf(destAddr2)).to.equal(255)Tümünü gösterİki yeni işlevi test edin.
transferFromTx öğesinin iki adres parametresi gerektirdiğini unutmayın: yetkiyi veren ve alan.
Sonuç
Hem Optimismopens in a new tab hem de Arbitrumopens in a new tab, L1'e yazılan çağrı verisinin boyutunu ve dolayısıyla işlem maliyetini düşürmenin yollarını arıyor. Ancak, genel çözümler arayan altyapı sağlayıcıları olarak yeteneklerimiz sınırlıdır. Merkeziyetsiz uygulama geliştiricisi olarak, uygulamaya özel bilgiye sahipsiniz, bu da çağrı verilerinizi genel bir çözümde yapabileceğimizden çok daha iyi optimize etmenizi sağlar. Umarım bu makale ihtiyaçlarınız için ideal çözümü bulmanıza yardımcı olur.
Çalışmalarımdan daha fazlası için buraya bakınopens in a new tab.
Sayfanın son güncellenmesi: 22 Ağustos 2025