1. Anasayfa
  2. Solidity

Solidity Eğitimi: Veri Konumları Hakkında Her Şey

Solidity Eğitimi - Depolama, bellek ve çağrı verilerine derinlemesine bir bakış

Solidity Eğitimi: Veri Konumları Hakkında Her Şey
Solidity Eğitimi
0

Solidity Eğitimi – Bu uzun, ayrıntılı ve derinlemesine bir okumadır. Ama korkma. Güzel bir kahve ya da en sevdiğiniz içecekle yerinizi ayırın. Her şey iyi olacak.

İlgilendiğiniz bölüme atlamak için aşağıdaki “İçindekiler” bölümüne bakın.

Bugün, Solidity‘nin yeni ve önemli bir bölümünü keşfediyoruz: veri konumları. Zorlu konu. Ethereum Sanal Makinesinin (EVM) mimarisiyle ilgili olduğu için çok düşük seviye.

Ancak analojilerle karmaşık programlama kavramlarını, mimarileri, akıllı sözleşmeleri ve genel olarak blok zinciri anlamada her zaman daha iyi olabiliriz.

Bu yazıda her bir veri konumunu EVM’nin “büyük bir endüstriyel fabrika” olup olmadığına bakarak öğreneceğiz (Endişelenmeyin, aşağıda göreceksiniz ve umarım anlamanıza yardımcı olmuşumdur).

Ardından, referans türleriyle her bir veri konumunun kurallarını ve davranışlarını öğrenmek için Solidity koduna geçeceğiz. Tüm bunlar, bildiğiniz popüler projelerden çizimler, mimari diyagramlar, kod parçacıkları ve kaynak kodu örnekleriyle desteklenir.

Solidity Eğitimi

  • Solidity’de EVM Veri Konumlarını neden anlamalıyız?
  • Veri Konumları → Genel Bakış
  • Veri Konumu → Kurallar
  • İşlev parametreleri için kurallar
  • İşlev gövdesi için kurallar
  • Veri Konumları → Davranışlar
  • Eşleme durumu (case of mappings)
  • Sonuç Bağlamı
  • Referanslar

Gelin hikayeye başlayalım; Kariyeri konusunda tutkulu bir adam olan, endüstriyel fırınların inşaatçısı ve yenileyicisi olan vatandaş, çocuğunu endüstriyel fabrikalarda çalışmaya göndermeye karar verdi.

Çok büyükler! Çelik ve alüminyumu eriten ve üreten fabrikalar.

Çelikhane ve haddehane gibi endüstriyel tesisin farklı bölümleri arasında gidip gelmek için bir araca ihtiyacınız var, o derece büyük bir işletme.

Ethereum ve EVM mimarisinin temsili bir diyagramı
Ethereum ve EVM mimarisinin temsili bir diyagramı – Kaynak

Ayrıca , haddehanenin bulunduğu hangara ilk girdiğinizde görülen Endüstriyel fırın o kadar büyük ki bir uçtan diğer uca yürümek birkaç dakika sürüyor!

Hangar, diğer inşaatçılar, duvar ustaları, elektrikçiler, etrafta dolaşan ve süren forkliftler ve devasa mekanik kollarla fırınlara giren ve çıkan şeyleri kontrol eden makineleri kontrol eden adamlarla dolu.

Hikayede ki kahraman gibi bizlerde farklı türde bir üretici / inşaatçıyız, bizim üretimimiz web3 özelinde. :) Ama bu endüstriyel fabrikaya dönüp baktığımızda, içerisinin EVM ‘nin içindekilerle pek çok ortak yanı olduğunu görüyoruz.

Malzemeler birçok farklı yerde depolanır, raflardan çıkarılır ve fırınlara ve diğer rastgele makinelere konur. Eritilecek, işlenecek veya işlenecekler.

EVM endüstriyel bir fabrikadır diyebiliriz. “Dijital Fabrika” niteliğinde ki WEB3 Fabrikalarına dair bir youtube içeriği hazırlıyorum, belki de siz bu içeriği okurken youtube kanalımda yayında olabilir, mutlaka kontrol et!

Temsili Fabrika Görseli Kaynak : Unsplash

“ABI Hakkında Her Şey” başlıklı makale de bahs edeceğimiz üzere, “akıllı sözleşmeler çok rahat küçük varlıklardır”.

Ancak akıllı sözleşmeler bir kez çağrıldığında çok aktif hale gelirler! Bayt kodu olarak temel mantıkları çalışır ve yürütülür. EVM’ye teşekkürler.

EVM asla gevşemez. Yazılım istemcisini çalıştıran her makinede sürekli olarak tekrarlanır. Bu, blok zincirinin durumunu güncellemek ve herkesi senkronize tutmak içindir.

Büyük bir sanayi fabrikası da aynı. Sürekli çalışıyor, 7/24. Kapıda her zaman insanlar vardır, hangarlarda işleri ayıklayan gece çalışanları, içeri girip çıkan kamyonlar, makineler ve fırınlar daha fazla malzemeyi eritir, yakar ve işler…

Bir akıllı sözleşmeyi çağırdığınızda, EVM kendi bayt kodundan talimat setini (= opcodes / işlem kodları) çalıştırır ve yürütür. Bu işlem kodlarından bazıları, EVM’ye farklı konumlardan/konumlara veri okuması ve yazması talimatını verir. EVM, işini doğru bir şekilde yapmak için bu çoklu veri konumlarına ihtiyaç duyar.

Bir fabrikada operasyonel işler ve malzemeler birden fazla yerde bulunabilir.

  • Forkliftler, fabrikada özel bir depolama alanında bulunan büyük ve uzun raflardan malzeme paletlerini alır. (bkz: storage / depolama )
  • Operatörler, fırın kapısından geçemeyen büyük çelik/alüminyum parçalarını daha küçük parçalara ayırmak için kontrol odalarından makineleri ve robotları kontrol eder. (bkz: memory / hafıza )
  • İşlenecek malzemeler taşıyıcılar (kamyonlar veya gemiler) aracılığıyla, yalnızca içeriğini bilebileceğiniz/görebileceğiniz ancak dokunamayacağınız kapalı kaplarda teslim edilir. (calldata / çağrı verileri)
  • Konveyör bantlar sayesinde fabrikada daha fazla malzeme işlenmekte ve taşınmaktadır. (bkz: stack / yığın )
  • Son olarak, fabrikanın kendi trafik kuralları vardır. Orası yol levhalarıyla dolu! (bkz: code / kod )

Bir fabrika gibi, EVM hesaplamak ve işleme işini yapmak için birçok farklı alan kullanıyor.

EVM, endüstriyel bir fabrika gibidir.

Solidity Developer

Solidity’de EVM Veri Konumlarını Neden Anlamalısınız?

Her bir veri konumunun nasıl çalıştığını öğrenmek, storage, memory ve calldata‘nın yapısı ve düzeni veya “nelerin nerede saklanabileceği” gibi birden çok şeyi öğrenmeyi içerir.

Ama en önemlisi, size her biriyle ilişkili (gaz) maliyetini ve değişebilirlik/güvenlik değiş tokuşu dediğimiz detayı öğretir.

Bir Solidity Geliştiricisi olarak, EVM’deki veri konumlarını ve bunların nasıl uygun şekilde kullanılacağını iyi anlamak, şunları yapmanızı sağlayacaktır:

  • Akıllı sözleşmenizin performansını iyileştirin.
  • Yürütme maliyetini en aza indirin (genel veya dahili işlevleri çağırırken kullanılan gaz).
  • Güvenliği güçlendirin ve olası hataları önleyin.

Veri Konumları → Genel Bakış

Bu makale, verilerin yazılabileceği ve buradan okunabileceği bu farklı veri konumlarına iyi bir genel bakış sunmayı amaçlamaktadır. Bazı konumların salt okunur olduğunu ve yazılamadığını, bazılarının ise değişken olduğunu ve içinde saklanan değerlerin düzenlenebileceğini göreceğiz.

EVM, beş ana veri konumunu elden çıkarır:

  • Depolamak (Storage)
  • Hafıza (Memory)
  • Çağrı Verileri (calldata)
  • Yığın (stack)
  • Kod (code)
EVM’de bulunan veri konumlarına genel bakış – Kaynak

Ana EVM veri konumlarının temelleri ayrıca Ethereum Sarı Belgesi, “9) Yürütme Modeli > Temeller” bölümünde belirtilmiştir.

Ethereum Sarı Kağıt Belgesi Görseli – Kaynak : ETH Yellow Paper

Depolamak

Akıllı sözleşmeli depolama, endüstriyel bir hangardaki depolama raf birimlerine eşdeğerdir.

Ethereum’da, belirli bir adresteki her akıllı sözleşmenin, 256 bitlik kelimeleri 256 bitlik kelimelere eşleyen bir anahtar-değer deposundan oluşan kendi storage‘i vardır. Depolamadaki veriler, işlev çağrıları ve işlemler arasında devam eder.

Depolama, tüm sözleşme durumu değişkenlerinin bulunduğu yerdir. Her sözleşmenin kendi deposu vardır. Depolamada depolanan değişkenler, işlev çağrıları arasında kalır. Ancak, depolama kullanımı oldukça pahalıdır.

Depolama, sözleşmeli depolamayı ifade ettiğinden, blok zincirinde kalıcı olarak depolanan verileri ifade eder.

Sözleşme deposuna okuyabilir ve yazabilirsiniz. Düşük düzeyde, bunu yapmak için kullanılan EVM işlem kodları SSTORE ve SLOAD.

Hafıza

EVM memory, geçici değerleri tutmak için kullanılır ve harici fonksiyon çağrıları arasında silinir. Ancak kullanımı daha ucuzdur.

EVM’de bellek geçicidir ve belirli bir sözleşmenin (ortam) bağlamına özgüdür. Yani, yürütme bağlamı bir sözleşmeden diğerine değiştiğinde beyaz tahta/karalama defteri temizlenir. Her yeni mesaj çağrısında yeni temizlenmiş bir bellek örneği elde edilir.

Bu nedenle, bellek değişkenleri geçicidir. Diğer sözleşmelere yapılan harici işlev çağrıları arasında silinirler.

EVM hafızasından okuma ve yazma yapabilirsiniz. Düşük düzeyde, bellekten/belleğe okumak ve bellekten yazmak için kullanılabilen EVM işlem kodları MLOAD, MSTORE ve MSTORE8.

CALL gibi belirli EVM işlem kodları DELEGATECALL veya STATICCALL argümanlarını EVM belleğinden tüketir.

Çağrı Verileri

calldata, bir gemi veya kamyondan alınan bir konteynere eşdeğerdir. Bu kaplar, işlenmek üzere fabrikaya teslim edilen malzemeleri içerir. Çağrı verileri salt okunurdur.

Bir calldata işlemin verilerinin veya harici bir işlev çağrısının parametrelerinin bulunduğu yerdir. Salt okunur bir veri konumudur. Ona yazamazsınız.

Çağrı verileri çoğunlukla bellek gibi davranır ve bayt adreslenebilir bir alandır. Okumak istediğiniz bayt sayısı için tam bir bayt ofseti belirtmeniz gerekir.

Düşük düzeyde, çağrı verilerinden okumak için kullanılabilen EVM işlem kodları CALLDATALOAD, CALLDATASIZE ve CALLDATACOPY.

Yığın

Yığın, küçük yerel değişkenleri tutmak için kullanılır. Kullanımı neredeyse ücretsizdir (çok düşük miktarda gaz kullanın), ancak boyut olarak sınırlıdır ve sınırlı sayıda eşya alabilir.

Yığın, bir işlev içinde oluşturulan yerel değişkenlerin çoğunun bulunduğu yerdir. EVM’nin önemli bir parçasıdır.

Düşük seviyede , yığın üzerinde çalışmak için kullanılabilecek EVM işlem kodları PUSH, POP, SWAP ve DUP komutlarıdır. Diğer EVM işlem kodlarının çoğu, onu yığından tüketir (bunları yığından çıkararak) ve sonucu yığına geri gönderir.

Kod

Kod, sözleşme bayt kodunu ifade eder. Yalnızca sözleşme bayt kodunu okuyabilir, ona yazamazsınız. Burası genellikle Solidity‘de olduğu gibi tanımlanan değişkenleri bulabileceğiniz yerdir constant. EVM işlem kodlarının çoğu, argümanlarını yığından tüketir.

Bayt kodu, gönderici ve sözleşme meta verileri dahil olmak üzere sözleşme hakkında birçok bilgi ve mantık içerir.

Düşük seviyede, akıllı sözleşmenin kodundan okumak için kullanılabilecek EVM işlem kodları CODESIZE ve CODECOPY. İşlem kodları EXTERNALCODESIZE ve EXTERNALCODECOPY.

Veri Konumu — Kurallar

Değişkenler için varsayılan konumlar

Solidity Programlama Dili, tanımlandıkları yere bağlı olarak, bazı değişkenlerin varsayılan olarak nerede olması gerektiği etrafında bazı varsayılan kurallar tanımlar.

sabit olarak tanımlanan değişkenler = sözleşme kodu (= bayt kodu).

Bu değişkenler değişmezdir ve sözleşme dağıtıldıktan sonra değiştirilemez. Salt okunurdurlar ve satır içi yapılabilirler.

durum değişkenleri (fonksiyonların dışında bildirilir) = varsayılan olarak depoda.

Bunlara durum değişkenleri denir, çünkü bunlar sözleşme durumunun bir parçasıdır ve sırayla dünya durumunun bir parçasıdır (=Ethereum’daki tüm akıllı sözleşmelerin durumu). Bu değişkenler blok zincirine kalıcı olarak yazılır.

yerel değişkenler (bir fonksiyonun gövdesi içinde bildirilir) = yığında.

Değer türlerinin değişkenleri (örneğin, uint256, bytes8, address) yığında bulunur.

Çoğu zaman, veri konumu anahtar sözcüklerini (depolama, bellek veya çağrı verileri) kullanmanız gerekmeyecektir, çünkü Solidity konumu yukarıda açıklanan varsayılan kurallara göre işler.

Ancak, bu anahtar kelimeleri kullanmanız ve veri konumunu belirtmeniz gereken zamanlar vardır, yani yapı ve diziler gibi karmaşık türdeki değişkenlerle işlevler içinde çalışırken.

Referans Türleri

Diziler (uint256[] gibi sabit veya dinamik boyutlu diziler), bayt , dize, yapı ve eşlemeler için, değerin depolandığı veri alanını açıkça sağlamanız gerekir. Bu, depolama, bellek veya çağrı verileri olabilir.

Bu anahtar kelimelerden birini kullanarak, referans türünde bir değişken yaratıyorsunuz. Bu tür türler, değer türlerinden daha dikkatli ele alınmalıdır.

Doğal olarak ortaya çıkması gereken bir sonraki soru şu olacaktır:

Depolama, bellek ve çağrı verileri anahtar kelimeleri ne zaman kullanılır?

Solidity 0.5.0’dan önce, bu değişken bir fonksiyon parametresi olarak geçirildiğinde, karmaşık tipte bir değişken (örneğin, bir dinamik boyut dizisi) için veri konumunu belirtmemek mümkündü.

Veri konumunu nerede belirtebilirsiniz?

Yalnızca bir işlevde 3 yerde bir değişkene nereye başvurulacağının veri konumunu belirtebilirsiniz:

  • Parametreler için (= fonksiyon tanımı)
  • Fonksiyon içindeki yerel değişkenler için (= fonksiyon gövdesi)
  • Dönüş değerleri her zaman hafızadadır (= fonksiyon tanımı).

İşlev parametreleri için kurallar

Depolama, bir işlev parametresi için referans olarak kullanıldığında, sözleşme depolamasına yönelik bir işaretçidir.

Bellek ve çağrı verileri için aynı. Bu tür anahtar sözcükler, işlemden gelen EVM belleğindeki veya giriş verisi alanındaki (= calldata) bazı konumlara işaretçiler oluşturur.

İşlev Gövdesi için Kurallar

İşlevlerin içinde, işlev görünürlüğü ne olursa olsun üç veri konumunun tümü belirtilebilir.

Ancak, referans türleri arasındaki atamalar belirli kurallara bağlıdır. (İşte burada karmaşıklaşıyor ve “biraz dil bükülüyor!”).

  • storagedepolama referansları: her zaman sözleşme depolamasından doğrudan (=durum değişkenleri) veya başka bir depolama referansı aracılığıyla bir değer atanabilir, ancak bunlara bir bellek veya çağrı verisi referansı atanamaz
  • memory hafıza referansları: herhangi bir şey atanabilir (doğrudan durum değişkenleri veya depolama, hafıza veya çağrı verisi referansları). Bu her zaman bir kopya oluşturur.
  • calldataçağrı verisi referansları: her zaman çağrı verilerinden doğrudan (= tx/mesaj çağrı girişi) veya başka bir çağrı verisi referansı yoluyla bir değer atanabilir, ancak bunlara bir depolama veya hafıza referansı atanamaz.

Daha basit anlatmak gerekirse:

Bellek için = her zaman bellekteki herhangi bir veriyi kopyalayabiliriz (ister sözleşmenin deposundan isterse çağrı verilerinden gelsin).

Depolama ve calldata için = yalnızca belirtilen veri konumundan gelen değerleri atayabiliriz (doğrudan veya aynı türden bir referans yoluyla).

Depolama Referansları (Storage References) Örneği

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract StorageReferences {

    bytes someData;

    function storageReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // storage variables can reference storage variables as long as the storage reference they refer to is initialized.
        bytes storage d = a;

        // if the storage reference it refers to was not initiliazed, it will lead to an error
        //  "This variable (refering to a) is of storage pointer type and can be accessed without prior assignment, 
        //  which would lead to undefined behaviour."
        // basically you cannot create a storage reference that points to another storage reference that points to nothing
        // f -> e -> (nothing) ???
        /// bytes storage e;
        /// bytes storage f = e;

        // storage pointers cannot point to memory pointers (whether the memory pointer was initialized or not
        /// bytes storage x = b;
        /// bytes memory r = new bytes(3);
        /// bytes storage s = r;

        // storage pointer cannot point to a calldata pointer (whether the calldata pointer was initialized or not).
        /// bytes storage y = c;
        /// bytes calldata m = msg.data;
        /// bytes storage n = m;
    }

}

Veri Konumları Referansları (Data Locations References) Örneği

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract DataLocationsReferences {

    bytes someData;

    function memoryReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // this is valid. It will copy from storage to memory
        bytes memory d = a;

        // this is invalid since the storage pointer x is not initialized and does not point to anywhere.
        /// bytes storage x;
        /// bytes memory y = x;

        // this is valid too. `e` now points to same location in memory than `b`;
        // if the variable `b` is edited, so will be `e`, as they point to the same location
        // same the other way around. If the variable `e` is edited, so will be `b`
        bytes memory e = b;

        // this is invalid, as here c is a calldata pointer but is uninitialized, so pointing to nothing.
        /// bytes memory f = c;

        // a memory reference can point to a calldata reference as long as the calldata reference
        // was initialized and is pointing to somewhere in the calldata.
        // This simply result in copying the offset in the calldata pointed by the variable reference
        // inside the memory
        bytes calldata g = msg.data[10:];
        bytes memory h = g;

        // this is valid. It can copy the whole calldata (or a slice of the calldata) in memory
        bytes memory i = msg.data;
        bytes memory j = msg.data[4:16];
    }

}

Çağrı Verileri Referans (call data References) Örneği

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract DataLocationsReferences {

    bytes someData;

    function calldataReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // for calldata, the same rule than for storage applies.
        // calldata pointers can only reference to the actual calldata or other calldata pointers.
    }
}

Bellek ← Durum Değişkeni

contract MemoryCopy {
    bytes someData;
    constructor() {
        someData = bytes("All About Solidity");
    }
    function copyStorageToMemory() public {
        
        // assigning memory <-- storage
        // this load the value from storage and copy in memory
        bytes memory value = someData;
        // changes are not propagated down in the contract storage
        value = bytes("abcd");
    
    }
}

Bellek referanslı bir değişkene bir durum değişkeni atadığımızda, temelde depodan → belleğe veri kopyalarız.

= belleğe yazıyoruz = yeni bellek tahsis edildi.

Bu, değişkende yapılan herhangi bir değişikliğin sözleşme depolamasına (= sözleşme durumu) yayılmadığı anlamına gelir.

= sözleşme depolaması geçersiz kılınmayacak.

Yukarıdaki örnekte, işlevi çalıştırdıktan sonra, someData durum değişkeni değiştirilmemiştir.

durum değişkeni ← bellek

Bu, bir önceki “bellek ← durum değişkeni”nin karşı örneğidir. Bu örnek, Solidity belgeleri bölümünde bulunabilir.

Depolama İşaretçileri

contract StoragePointer {
    uint256[] someData;
    uint256[] moreData;
    function createStoragePointer() public {
        
        // pointer to storage
        uint256[] storage value = someData;
        // pointer to somewhere else in storage
        value = moreData;
     }
}

Bir işlev içinde bir depolama değişkeni oluşturulduğunda, bu temelde bir depolama işaretçisi görevi görür. Depolama işaretçisi, depolamada önceden tahsis edilmiş verilere başvurur.

Depolama işaretçisini, depoda başka bir yere yönlendirmek için yeniden atayabilirsiniz.

= depodaki bazı mevcut değerlere başvurur = yeni depolama oluşturulmaz

Ancak, doğrudan arama değişkenine yeni bir değer atayarak sözleşme deposunu geçersiz kılabiliriz. Bu örneğe bir göz atın:

contract StoragePointer {
    uint256[] public someData;
    function pushToArrayViaPointer() public {
       
        uint256[] storage value = someData;
        value.push(1);
    }
}

Daha sonra someData‘nın ilk dizinini sorgularsanız, 1 elde edersiniz.

Bellek ← Depolama İşaretçisi

Bellek referanslı bir değişkene depolama referanslı bir veri atadığımızda, depolama → hafızadan veri kopyalıyoruz.

= belleğe yazıyoruz = yeni bellek tahsis edildi.

Bu, bir depolama referansı aracılığıyla “arada ek bir yol” eklenmesiyle daha önce ele aldığımız ilk durum “bellek ← durum değişkeni“ne eşdeğerdir. Depolama referansı, daha sonra belleğe kopyalanacak olan durum değişkenine çözümlenecektir. İşte önceki kod parçacığına dayalı temel bir örnek.

contract MemoryCopy {
    uint256[] public someData;
    function copyStorageToMemoryViaRef() public {
    
        uint256[] storage storageRef = someData;
        uint256[] memory copyFromRef = storageRef;
    }
}

Burada copyFromRef, someData dizisinin tamamının bir kopyasıdır. Dizi, depolama referansı storageRef aracılığıyla belleğe kopyalandı.

Yine bu senaryoda, depodan belleğe kopyaladığımız için, depoda bulunan gerçek veriler üzerinde değil, verilerin bir kopyası üzerinde çalışıyoruz. Bu nedenle, copyFromRef‘te yapılan herhangi bir değişiklik, sözleşme deposuna geri yayılmayacak ve sözleşme durumunu değiştirmeyecektir.

Örneklemek için, aşağıdaki sözleşmeyi Remix’e kopyalayın ve:

  • test() fonksiyonunu çalıştırın.
  • index 1’deki someData dizisini okuyun.
contract MemoryCopy {
    uint256[] public someData;
    constructor() public {
        someData.push(1);
        someData.push(2);
        someData.push(3);
    }
    function doesNotPropagateToStorage() public {
        uint256[] storage storageRef = someData;
        uint256[] memory copyFromRef = storageRef;
        
        copyFromRef[1] = 12345;
    }
}

İşlevi çalıştırdıktan sonra someData[1]‘daki değerin hala 2 olduğunu ve 12345’in sözleşme deposuna geri yayılmadığını göreceksiniz.

Çağrı Verisi Referansı

Bir çağrı verisi başvurusu, bir depolama başvurusuyla aynı şekilde davranır. Yalnızca işlem verilerine referans olarak veya calldata anahtar sözcüğüyle sağlanan karmaşık türdeki işlev bağımsız değişkenlerini işlev görebilir.

Kısaca, calldata türünde bir değişken her zaman bir referans oluşturur.

Tek temel fark, çağrı verileri salt okunur olduğundan, çağrı verileri referansları olan değişkenlerin değiştirilememesidir.

Bu, depolama referansının tersidir. Bir depolama referansına yeni bir değer atadığınızda, değişiklik sözleşme durumuna geri yayılır.

Veri Konumu — Davranışlar

Solidity belgeleri şçyle der; “Veri konumları yalnızca verilerin kalıcılığıyla değil, aynı zamanda atamaların anlambilimiyle de ilgilidir.

İşlev gövdesi içindeki veri konumunu belirlerken iki ana şey göz önünde bulundurulmalıdır: etki ve gaz kullanımı.

Daha iyi anlamak için örnek olarak basit bir sözleşme kullanalım. Bu sözleşme, depodaki yapı öğelerinin bir eşlemesini içerir. Her bir veri konumunun davranışını karşılaştırmak için, farklı veri konumu anahtar sözcükleri kullanan farklı işlevler kullanacağız.

  • depolama kullanan bir alıcı.
  • bellek kullanan bir alıcı.
  • depolama kullanan bir ayarlayıcı.
  • bellek kullanan bir ayarlayıcı.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

contract Garage {
    struct Item {
        uint256 units;
    }
    mapping(uint256 => Item) items;

    // gas (view): 24,025
    function getItemUnitsStorage(uint _itemIndex) public view returns(uint) {
        Item storage item = items[_itemIndex];
        return item.units;
    }

    // gas (view): 24,055
    function getItemUnitsMemory(uint _itemIndex) public view returns (uint) {
        Item memory item = items[_itemIndex];
        return item.units;
    }

    // gas: 50,755 (1st storage write)
    function setItemUnitsStorage(uint256 _id, uint256 _units) public 
    { 
        Item storage item = items[_id];
        item.units = _units;
    }

    // gas: 27,871 
    function setItemUnitsMemory(uint256 _id, uint256 _units) public   
    {
        Item memory item = items[_id];
        item.units = _units;
    }
}

Depolama ve Bellek ile Getter

Kendimize sormamız gereken ilk soru şudur:

  • Bellekli veya depolamalı bir alıcı kullanmak daha mı ucuz?
  • Her biri nasıl çalışır?

// gas (view): 24,025
    function getItemUnitsStorage(uint _itemIndex) public view returns(uint) {
        Item storage item = items[_itemIndex];
        return item.units;
    }
    // gas (view): 24,055
    function getItemUnitsMemory(uint _itemIndex) public view returns (uint) {
        Item memory item = items[_itemIndex];
        return item.units;
    }

Depolama işaretçisi görevi gördüğü için, depolamalı alıcı, hafızalı olandan biraz daha ucuzdur.

Bellek kullanan bir alıcı daha pahalıdır ve yeni bir değişken oluşturulduğu için daha fazla gaz maliyetine mal olur. Yukarıdaki örnekte, depolama eşleme öğeleri[_itemIndex] içindeki değeri okumaya ek olarak, değeri bellekte kopyalayacaktır.

Burada neler olduğunu tam olarak anladığımızdan emin olalım. EVM, storage veya memory anahtar kelimesine bağlı olarak işin detayında neler var ona bakalım…

Her iki alıcı türü için de işlem kodlarının sırasını aşağıya koydum (açıklık + kısalık için soldaki Program Sayacı olmadan). Remix’te kodun hatalarını ayıklayarak bunları inceleyebilirsiniz.

Her iki alıcı işlevinin işlem kodlarını karşılaştırırsak, depolamalı bir alıcının, bellek kullanan bir alıcıya (47 talimat) kıyasla daha az işlem kodu (30 talimat) içerdiğini fark ederiz.

Öneri: Bu iki işlevi test etmek ve opcode’ları ve depolama/bellekteki değişiklikleri birbiri ardına analiz etmek için evm.codes web sitesini kullanmanızı şiddetle tavsiye ederim.

; getItemUnitsStorage = 30 instructions
PUSH1 00   ; 1) manipulate + prepare the stack
DUP1
PUSH1 00   
DUP1
DUP5       
DUP2
MSTORE     ; 2.1) prepare the memory for hashing (1)
PUSH1 20   
ADD    
SWAP1
DUP2 
MSTORE     ; 2.2) prepare the memory for hashing (2)
PUSH1 20
ADD
PUSH1 00
SHA3       ; 3) compute the storage number to load via hashing
SWAP1
POP
DUP1
PUSH1 00
ADD
SLOAD      ; 4) load mapping value from storage
SWAP2    
POP
POP
SWAP2
SWAP1
POP
JUMP
JUMPDEST
; getItemUnitsMemory = 47 instructions
PUSH1 00
DUP1
PUSH1 00
DUP1
DUP5
DUP2
MSTORE
PUSH1 20
ADD
SWAP1
DUP2
MSTORE
PUSH1 20
ADD
PUSH1 00
SHA3
PUSH1 40  ; <------ additional opcodes start here
MLOAD     ; 1) load the free memory pointer
DUP1      ; 2) reserve the free memory pointer by duplicating it
PUSH1 20
ADD       ; 3) compute the new free memory pointer
PUSH1 40
MSTORE    ; 4) store the new free memory pointer
SWAP1
DUP2
PUSH1 00
DUP3
ADD
SLOAD     ; 5) load mapping value from storage
DUP2
MSTORE    ; 6) store mapping value retrieved from storage in memory
POP
POP ; <------------ additional opcodes end here
SWAP1
POP
DUP1
PUSH1 00
ADD
MLOAD
SWAP2
POP
POP
SWAP2
SWAP1
POP

Kaputun altında, EVM, depolama kullanan bir alıcı için aşağıdaki adımları gerçekleştirir:

  • manipüle et + yığını hazırla
  • hafızayı karma için hazırlayın ( (1) ve (2) )
  • karma ve SHA3 yoluyla yüklenecek değerin depolama yuvasını hesaplayın
  • Değeri, SLOAD aracılığıyla depolamadan yükleyin.

Ardından gelen soru şu:

Neden hafızalı bir alıcının düşük seviyede 17 opcode talimatı daha var?

Cevap, yukarıdaki montaj kodunun açıklama yorumunda yatmaktadır. 17 ek EVM talimatı aşağıdakileri gerçekleştirir:

  1. Öncelikle;
  • 1.1 boş bellek işaretçisini yükleyerek,
  • 1.2 ayırarak,
  • 1.3 bellekteki bir sonraki boş alanı hesaplayarak ve
  • 1.4 yeni boş bellek işaretçisini güncelleyerek, değeri depolamak için bellekte bir miktar alan ayırır.

(MLOAD + MSTORE)

2. Sonrasında;

Değer, SLOAD aracılığıyla sözleşme deposundan yüklendikten sonra MSTORE aracılığıyla belleğe yazılır.

Depolama ve bellek ile ayarlayıcı

// gas: 50,755 (1st storage write)
    function setItemUnitsStorage(uint256 _id, uint256 _units) public 
    { 
        Item storage item = items[_id];
        item.units = _units;
    }
    // gas: 27,871 
    function setItemUnitsMemory(uint256 _id, uint256 _units) public   
    {
        Item memory item = items[_id];
        item.units = _units;
    }

Bir değişken için depolamayı belirtirken ve değerini ayarlarken, durum değişkenini değiştirir ve sözleşme depolamasını geçersiz kılar. Bu, SSTORE işlem kodunu kullanacaktır.

Aksine, bellek anahtar sözcüğü kullanılırsa, yalnızca yerel değişkeni geçersiz kılar ve sözleşme depolamasını geçersiz kılmaz. Bu, MSTORE işlem kodunu kullanacaktır.

EVM komut seti söz konusu olduğunda, aşağıdaki snippet’ten, depolama kullanmanın çok daha az talimat oluşturduğunu görebiliriz (depolama için 28, bellekle 48’e kıyasla). Ancak, depolama okuma/yazma EVM’deki en pahalı işlemler olduğundan, depolamayı kullanmak çok daha fazla gaza mal olacaktır.

; setItemUnitsStorage = 28 instructions
PUSH1 00
DUP1
PUSH1 00
DUP5
DUP2
MSTORE
PUSH1 20
ADD
SWAP1
DUP2
MSTORE
PUSH1 20
ADD
PUSH1 00
SHA3
SWAP1
POP
DUP2
DUP2
PUSH1 00
ADD
DUP2
SWAP1
SSTORE
POP
POP
POP
POP
; setItemUnitsMemory = 46 instructions
PUSH1 00
DUP1
PUSH1 00
DUP5
DUP2
MSTORE
PUSH1 20
ADD
SWAP1
DUP2
MSTORE
PUSH1 20
ADD
PUSH1 00
SHA3
PUSH1 40
MLOAD
DUP1
PUSH1 20
ADD
PUSH1 40
MSTORE
SWAP1
DUP2
PUSH1 00
DUP3
ADD
SLOAD
DUP2
MSTORE
POP
POP
SWAP1
POP
DUP2
DUP2
PUSH1 00
ADD
DUP2
DUP2
MSTORE
POP
POP
POP
POP
POP

(Edge) Eşleme Örneği

mapping, işlevlere parametre olarak geçirilebilir veya işlev gövdelerinin içinde tanımlanabilir. Bununla birlikte, bununla ilgili iki özel kuralı olan bir uç durumdur:

  • bunlara yalnızca, sözleşme deposunda zaten var olan bir eşlemeye referans olarak işlev görmesi için veri konumu storage atanabilir.
  • bir değer ile başlatılmaları gerekir.

Bunun nedeni, eşlemenin dinamik olarak oluşturulamamasıdır. Durum değişkeni olarak zaten var olan bir eşlemeye referans olarak işlev görmek için yalnızca işlev gövdesi içinde tanımlanabilirler.

Sonuç Bağlamı : Veri Konumları Hakkında

Depolama, bellek veya çağrı verilerini kullanmanız gerekip gerekmediği, sözleşmenizde ne yapmaya çalıştığınıza bağlıdır.

Belirli veri türleri için, veriler büyükse, bunları depolamadan belleğe kopyalamak oldukça pahalı olabilir.

Ancak bazı durumlarda, değişkeni işlev içinde geçersiz kılmak ve sonuçları sözleşme deposuna yaymak istemiyorsanız bu gerekli olabilir.

Öte yandan, çağrı verileri belleğe göre çeşitli avantajlar sunar.

  • Verileri belleğe kopyalamaktan kaçınarak, gaz bedelinden tasarruf edin.
  • calldata salt okunur bir veri konumu olduğundan, verilerin değiştirilememesini sağlayabilir. Bu, özellikle işleve iletilen argüman “hassas” verileri temsil ettiğinde (örneğin 64 bayt imzalar gibi) güvenliği artırabilir, yani bazı durumlarda “daha güvenli” olabilir.

Ancak, calldata üzerinde memory kullanmak bazı senaryolarda birleştirilebilirliği artırabilir.

Son olarak, işlevinizde uygun veri konumunu kullanmamanın olası hatalara ve güvenlik açıklarına yol açabileceğini unutmayın. Gerçek dünyadan bir örnek, Cover protokolüne yapılan sonsuz bir hack saldırısıydı.

Cover protokolünden Blacksmith.sol sözleşmesindeki bu kod parçacığına bir göz atın.

Cover protocol, Blacksmith Contract, Hata Göstergeleri
Cover protocol, Blacksmith Contract, Hata Göstergeleri

Bu makale “Solidity Veri Konumları Hakkında” oluşturulan içeriklerden derlenmiştir.

Akıllı sözleşme geliştirme yolculuğunuz hakkında daha iyi rehberlik almak için Solidity nedir? Ethereum Akıllı Sözleşmelerinin Dili Rehberi içeriğimize göz atın. Dilerseniz Yeni Başlayanlar için Solidity – Akıllı Sözleşme Geliştirme Hızlandırılmış Kursuna katılın.

Çalışmaya nereden başlayacağım diyenler için Blockchain ​​Developer Olmak İçin Yol Haritası içeriğine de muhakkak bakın.

Bu makaleyi okuduğunuz için teşekkürler! Bana destek olmak isterseniz;

Beni TwitterLinkedin ve YouTube‘da takip edin.

Kısa bir yorum bırakmayı UNUTMAYIN!

Hasan YILDIZ, Girişimci. Doktora Öğrencisi. Yazmayan YAZILIMCI. Veri Şeysi. Eğitmen...

Yazarın Profili
İlginizi Çekebilir

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir