Sözleşme boyutu sınırıyla mücadele etmek için sözleşmelerin küçültülmesi
22 Kasım 2016(opens in a new tab)'da Spurious Dragon sert çatalı 24,576 kb akıllı sözleşme boyutu sınırı ekleyen EIP-170(opens in a new tab)'i tanıttı. Bir Solidity geliştiricisi olarak sizin için bu, sözleşmenize giderek daha fazla işlevsellik eklediğinizde, bir noktada sınıra ulaşacağınız ve dağıtım sırasında şu hatayı göreceğiniz anlamına gelir:
Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). Bu sözleşme Mainnet'te dağıtılamayabilir. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
Bu sınır, hizmet reddi (DOS) saldırılarını önlemek için getirildi. Bir sözleşmeye yapılan herhangi bir çağrı, gaz açısından nispeten ucuzdur. Bununla birlikte, Ethereum düğümleri için bir sözleşme çağrısının etkisi, çağrılan sözleşme kodunun boyutuna bağlı olarak orantısız bir şekilde artar (kodu diskten okumak, kodu önceden işlemek, Merkle kanıtına veri eklemek). Saldırganın başkaları için çok iş yapmak için az kaynağa ihtiyaç duyduğu böyle bir durumunuz olduğunda, DOS saldırıları potansiyeli elde edersiniz.
Bir doğal sözleşme boyutu limiti, blok gaz limiti olduğu için başlangıçta bu çok da büyük bir problem değildi. Açıkça görülüyor ki bir sözleşmenin, sözleşmenin tüm bit kodunu tutan bir işlem içinde dağıtılması gerekir. Bir bloğa yalnızca bir işlemi dahil ederseniz bu gazın tamamını kullanabilirsiniz, ancak bu sonsuz değildir. Londra Yükseltmesi'nden bu yana blok gaz limiti, ağ talebine bağlı olarak 15 milyon ile 30 milyon birim arasında değişti.
Ne yazık ki, sözleşmelerinizin bayt kodu boyutunu almanın kolay bir yolu yok. Truffle kullanıyorsanız truffle-contract-size(opens in a new tab) eklentisi size yardımcı olacak iyi bir araçtır.
npm install truffle-contract-size
- Eklentiyi truffle-config.js öğesine ekleyin:
plugins: ["truffle-contract-size"]
truffle run contract-sizekomutunu çalıştırın
Bu, değişikliklerinizin toplam sözleşme boyutlarını nasıl etkilediğini anlamanıza yardımcı olacaktır.
Aşağıda, potansiyel etkilerine göre sıralanan bazı yöntemlere bakacağız. Bunu, kilo verme gibi düşünün. Birinin hedef kilosuna (bizim durumumuzda 24 kb) ulaşması için en iyi strateji, önce büyük etkiye sahip yöntemlere odaklanmaktır. Çoğu zaman sadece diyeti düzeltmek amaca ulaştırır ancak bazen biraz daha fazlası gerekir. Sonra biraz egzersiz (orta etki) veya hatta takviye besinler (küçük etki) ekleyebilirsiniz.
Bu her zaman ilk yaklaşımınız olmalıdır. Sözleşmeyi birden çok küçük sözleşmeye nasıl ayırabilirsiniz? Genellikle sizi sözleşmeleriniz için iyi bir mimari bulmaya zorlar. Daha küçük sözleşmeler her zaman kod okunabilirliği açısından tercih edilir. Sözleşmeleri bölmek için kendinize şunları sorun:
- Hangi fonksiyonlar birlikte olmalıdır? Her fonksiyon seti, en çok kendi sözleşmesine uyacaktır.
- Hangi fonksiyonlar, sözleşme durumunun okunmasını veya yalnızca durumun belirli bir alt kümesini gerektirmez?
- Depolamayı ve işlevselliği bölebilir misiniz?
Fonksiyon kodunu depolama alanından uzaklaştırmanın basit bir yolu, bir kütüphane(opens in a new tab) kullanmaktır. Kütüphane fonksiyonları derleme esnasında doğrudan sözleşmeye ekleneceği(opens in a new tab) için onları dahili olarak duyurmayın. Ancak genel fonksiyonları kullanırsanız, bunlar aslında ayrı bir kütüphane sözleşmesinde olacaktır. Kütüphanelerin kullanımını daha uygun hâle getirmek için using for(opens in a new tab)'u göz önüne alın.
Proxy sistemi, daha gelişmiş bir stratejidir. Kütüphaneler arka planda, çağıran sözleşmenin durumuyla başka bir sözleşmenin fonksiyonunu yürüten
DELEGATECALL kullanır. Proxy'ler hakkında dahasını öğrenmek için bu blog gönderisine(opens in a new tab) bakın. Yükseltilebilirliği sağlamak gibi daha fazla işlevsellik sağlarlar ancak aynı zamanda çok fazla karmaşıklık da eklerler. Herhangi bir nedenle tek seçeneğiniz olmadıkça, bunları yalnızca sözleşme boyutlarını azaltmak için eklenmesini tavsiye etmem.
Bu bariz bir yöntem. Fonksiyonlar, sözleşme boyutunu biraz artırır.
- Harici: Çoğu zaman kolaylık sağlamak için çok sayıda görüntüleme fonksiyonu ekleriz. Boyut sınırına ulaşana kadar bu gayet iyi bir yöntemdir. O zaman kesinlikle gerekli olanlar hariç hepsini kaldırmayı gerçekten düşünmek isteyebilirsiniz.
- Dahili: Ayrıca dahili/özel fonksiyonları kaldırabilir ve fonksiyon yalnızca bir kez çağrıldığı sürece kodu satır içine alabilirsiniz.
Bunun gibi küçük bir değişim:
1function get(uint id) returns (address,address) {2 MyStruct memory myStruct = myStructs[id];3 return (myStruct.addr1, myStruct.addr2);4}Kopyala
1function get(uint id) returns (address,address) {2 return (myStructs[id].addr1, myStructs[id].addr2);3}Kopyala
0,28kb'lık bir fark yaratır. Muhtemelen sözleşmelerinizde birçok benzer durum vardır ve bunlar gerçekten önemli miktarlara ulaşabilir.
Uzun geri dönüş mesajları ve özellikle birçok farklı geri dönüş mesajı, sözleşmeyi şişirebilir. Bunun yerine kısa hata kodları kullanın ve bunları sözleşmenizde çözün. Uzun bir mesaj çok daha kısa olabilir:
1require(msg.sender == owner, "Only the owner of this contract can call this function");2Kopyala
1require(msg.sender == owner, "OW1");Kopyala
Özel hatalar Solidity 0.8.4(opens in a new tab)'te tanıtılmıştır. Bu hatalar, sözleşmelerinizin boyutunu azaltmanın harika bir yoludur, çünkü seçiciler olarak ABI kodludur (tıpkı işlevler gibi).
1error Unauthorized();23if (msg.sender != owner) {4 revert Unauthorized();5}Kopyala
Optimize edici ayarlarını da değiştirebilirsiniz. 200 varsayılan değeri, bit kodunu bir fonksiyon 200 kez çağrılmış gibi optimize etmeye çalıştığı anlamına gelir. 1 olarak değiştirirseniz, temel olarak optimize ediciye her fonksiyonu yalnızca bir kez çalıştırma durumu için optimize etmesini söylersiniz. Yalnızca bir kez çalışmak için optimize edilmiş bir fonksiyon, dağıtımın kendisi için optimize edildiği anlamına gelir. Bunun, işlevleri çalıştırmak için gereken gaz maliyetlerini artırdığını unutmayın, yani bunu yapmamak daha iyi olabilir.
Eğer ABIEncoderV2(opens in a new tab) kullanıyorsanız bu, fonksiyonlara yapı aktarmamanıza yardımcı olabilir. Parametreyi bir yapı olarak aktarmaktansa...
1function get(uint id) returns (address,address) {2 return _get(myStruct);3}45function _get(MyStruct memory myStruct) private view returns(address,address) {6 return (myStruct.addr1, myStruct.addr2);7}Kopyala
1function get(uint id) returns(address,address) {2 return _get(myStructs[id].addr1, myStructs[id].addr2);3}45function _get(address addr1, address addr2) private view returns(address,address) {6 return (addr1, addr2);7}Kopyala
...gerekli parametreleri doğrudan aktarın. Bu örnekte 0,1 kb daha kazandık.
- Yalnızca dışarıdan çağrılan fonksiyonlar veya değişkenler ne olacak? Onları
publicyerine
externalolarak duyurun.
- Yalnızca sözleşmenin içinden çağrılan fonksiyonlar veya değişkenler ne olacak? Onları
publicyerine
privateveya
externalolarak duyurun.
Niteleyiciler, özellikle yoğun olarak kullanıldığında sözleşme boyutu üzerinde önemli bir etkiye sahip olabilir. Onları kaldırmayı ve yerine fonksiyon kullanmayı göz önünde bulundurun.
1modifier checkStuff() {}23function doSomething() checkStuff {}Kopyala
1function checkStuff() private {}23function doSomething() { checkStuff(); }Kopyala
Bu ipuçları, sözleşme boyutunu önemli ölçüde azaltmanıza yardımcı olacaktır. Bir kez daha, en büyük etki için mümkünse her zaman sözleşmeleri bölmeye odaklanmanızı söylemem gerekiyor.
Son düzenleme: @kaanmetu(opens in a new tab), Invalid DateTime