Sözleşme boyutu sınırıyla mücadele etmek için sözleşmeleri küçültmek
Neden bir sınır var?
22 Kasım 2016 (opens in a new tab)'da Spurious Dragon sert çatallanması, 24.576 kb'lık bir 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 bu, sözleşmenize giderek daha fazla işlevsellik eklediğinizde bir noktada sınıra ulaşacağınız ve dağıtım yaparken şu hatayı göreceğiniz anlamına gelir:
Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. 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. Ancak, 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 ön işleme tabi tutmak, Merkle kanıtına veri eklemek). Saldırganın başkalarına çok fazla iş çıkarmak için az kaynağa ihtiyaç duyduğu böyle bir durum olduğunda, DOS saldırıları potansiyeli ortaya çıkar.
Başlangıçta bu daha az bir sorundu çünkü doğal bir sözleşme boyutu sınırı blok gaz limitiydi. Açıkçası, bir sözleşme, sözleşmenin tüm baytkodunu barındıran bir işlem içinde dağıtılmalıdır. Bir bloğa yalnızca o tek işlemi dahil ederseniz, tüm o gazı tüketebilirsiniz, ancak bu sonsuz değildir. London Yükseltmesi'nden bu yana, blok gaz limiti ağ talebine bağlı olarak 15M ile 30M birim arasında değişebilmektedir.
Aşağıda, potansiyel etkilerine göre sıralanmış bazı yöntemlere bakacağız. Bunu kilo verme açısından düşünün. Birinin hedef kilosuna (bizim durumumuzda 24kb) ulaşması için en iyi strateji, önce büyük etkili yöntemlere odaklanmaktır. Çoğu durumda sadece diyetinizi düzeltmek sizi oraya ulaştıracaktır, ancak bazen biraz daha fazlasına ihtiyacınız olur. O zaman biraz egzersiz (orta etki) veya hatta takviyeler (küçük etki) ekleyebilirsiniz.
Büyük etki
Sözleşmelerinizi ayırın
Bu her zaman ilk yaklaşımınız olmalıdır. Sözleşmeyi birden fazla daha küçük sözleşmeye nasıl ayırabilirsiniz? Bu genellikle sizi sözleşmeleriniz için iyi bir mimari bulmaya zorlar. Daha küçük sözleşmeler, kod okunabilirliği açısından her zaman tercih edilir. Sözleşmeleri bölmek için kendinize şunları sorun:
- Hangi işlevler birbiriyle bağlantılı? Her bir işlev seti en iyi kendi sözleşmesinde yer alabilir.
- Hangi işlevler sözleşme durumunu okumayı gerektirmez veya sadece durumun belirli bir alt kümesini gerektirir?
- Depolama ve işlevselliği ayırabilir misiniz?
Kütüphaneler
İşlevsellik kodunu depolamadan uzaklaştırmanın basit bir yolu bir kütüphane (opens in a new tab) kullanmaktır. Kütüphane işlevlerini internal olarak bildirmeyin, çünkü bunlar derleme sırasında doğrudan sözleşmeye eklenecektir (opens in a new tab). Ancak public işlevler kullanırsanız, bunlar aslında ayrı bir kütüphane sözleşmesinde olacaktır. Kütüphanelerin kullanımını daha kolay hale getirmek için using for (opens in a new tab) kullanmayı düşünün.
Proxy'ler
Daha gelişmiş bir strateji bir proxy sistemi olacaktır. Kütüphaneler arka planda, çağıran sözleşmenin durumuyla başka bir sözleşmenin işlevini basitçe yürüten DELEGATECALL kullanır. Proxy sistemleri hakkında daha fazla bilgi edinmek için bu blog yazısına (opens in a new tab) göz atın. Size daha fazla işlevsellik sağlarlar, örneğin yükseltilebilirliği mümkün kılarlar, ancak aynı zamanda çok fazla karmaşıklık da eklerler. Herhangi bir nedenle tek seçeneğiniz olmadığı sürece, bunları yalnızca sözleşme boyutlarını küçültmek için eklemezdim.
Orta etki
İşlevleri kaldırın
Bu açık olmalıdır. İşlevler bir sözleşme boyutunu oldukça artırır.
- Harici (External): Çoğu zaman kolaylık sağlaması için birçok view işlevi ekleriz. Boyut sınırına ulaşana kadar bu tamamen sorunsuzdur. Ancak sınıra ulaştığınızda, kesinlikle gerekli olanlar dışındakileri kaldırmayı gerçekten düşünmek isteyebilirsiniz.
- Dahili (Internal): Ayrıca internal/private işlevleri kaldırabilir ve işlev yalnızca bir kez çağrıldığı sürece kodu doğrudan satır içine (inline) ekleyebilirsiniz.
Ek değişkenlerden kaçının
function get(uint id) returns (address,address) {
MyStruct memory myStruct = myStructs[id];
return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns (address,address) {
return (myStructs[id].addr1, myStructs[id].addr2);
}
Bunun gibi basit bir değişiklik 0.28kb'lık bir fark yaratır. Sözleşmelerinizde buna benzer birçok durum bulma ihtimaliniz yüksektir ve bunlar gerçekten önemli miktarlara ulaşabilir.
Hata mesajını kısaltın
Uzun geri alma (revert) mesajları ve özellikle birçok farklı geri alma mesajı sözleşmeyi şişirebilir. Bunun yerine kısa hata kodları kullanın ve bunları sözleşmenizde çözümleyin. Uzun bir mesaj çok daha kısa hale gelebilir:
require(msg.sender == owner, "Only the owner of this contract can call this function");
require(msg.sender == owner, "OW1");
Hata mesajları yerine özel hatalar kullanın
Özel hatalar Solidity 0.8.4 (opens in a new tab) ile tanıtılmıştır. Sözleşmelerinizin boyutunu küçültmek için harika bir yoldur, çünkü (tıpkı işlevler gibi) seçiciler olarak ABI kodlamasına tabi tutulurlar.
error Unauthorized();
if (msg.sender != owner) {
revert Unauthorized();
}
Optimize edicide düşük bir çalıştırma değeri düşünün
Ayrıca optimize edici (optimizer) ayarlarını da değiştirebilirsiniz. Varsayılan değer olan 200, baytkodu sanki bir işlev 200 kez çağrılıyormuş gibi optimize etmeye çalıştığı anlamına gelir. Bunu 1 olarak değiştirirseniz, temel olarak optimize ediciye her işlevi yalnızca bir kez çalıştırma durumu için optimize etmesini söylersiniz. Yalnızca bir kez çalıştırılmak üzere optimize edilmiş bir işlev, dağıtımın kendisi için optimize edildiği anlamına gelir. Bunun işlevleri çalıştırmak için gaz maliyetlerini artırdığını unutmayın, bu yüzden bunu yapmak istemeyebilirsiniz.
Küçük etki
İşlevlere yapı (struct) geçirmekten kaçının
ABIEncoderV2 (opens in a new tab) kullanıyorsanız, bir işleve yapıları (structs) geçirmemek yardımcı olabilir. Parametreyi bir yapı olarak geçirmek yerine, gerekli parametreleri doğrudan geçirin. Bu örnekte 0.1kb daha tasarruf ettik.
function get(uint id) returns (address,address) {
return _get(myStruct);
}
function _get(MyStruct memory myStruct) private view returns(address,address) {
return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns(address,address) {
return _get(myStructs[id].addr1, myStructs[id].addr2);
}
function _get(address addr1, address addr2) private view returns(address,address) {
return (addr1, addr2);
}
İşlevler ve değişkenler için doğru görünürlüğü bildirin
- Yalnızca dışarıdan çağrılan işlevler veya değişkenler mi var? Bunları
publicyerineexternalolarak bildirin. - Yalnızca sözleşme içinden çağrılan işlevler veya değişkenler mi var? Bunları
publicyerineprivateveyainternalolarak bildirin.
Değiştiricileri (modifiers) kaldırın
Değiştiriciler (modifiers), özellikle yoğun kullanıldığında, sözleşme boyutu üzerinde önemli bir etkiye sahip olabilir. Bunları kaldırmayı ve yerine işlevler kullanmayı düşünün.
modifier checkStuff() {}
function doSomething() checkStuff {}
function checkStuff() private {}
function doSomething() { checkStuff(); }
Bu ipuçları, sözleşme boyutunu önemli ölçüde küçültmenize yardımcı olacaktır. Bir kez daha, ne kadar vurgulasam azdır, en büyük etki için mümkünse her zaman sözleşmeleri bölmeye odaklanın.
Sayfanın son güncellenme tarihi: 3 Nisan 2026