1. Anasayfa
  2. Solidity

Solidity Eğitimi Calldata Hakkında Her Şey

Solidity'de bir Ethereum işleminin "veri" alanını anlama

Solidity Eğitimi Calldata Hakkında Her Şey
Solidity Eğitimi Calldata
0

Bugün, calldata‘nın özelliklerini ve onu neden bellek gibi diğer veri konumlarına göre ayrıcalıklı kılmanız gerektiğini öğreneceğiz. calldata ile ilgili üç EVM işlem kodunu anlamak için Gnosis güvenli sözleşmesindeki kod örneklerini kullanacağız.

Solidity Eğitimi Calldata

  • calldata Düzeni
  • calldata’nın Temelleri
  • Solidity’de calldata’ya erişme
  • Assembly ile Calldata işlem kodları
  • Düşük Seviyeli Aramalar için calldata Oluşturma
  • Bayt Calldata Dilimlerini Çıkarma
  • Dahili İşlevlerde calldata
  • Constructor’da calldata

web3.js veya ethers.js’ye aşina iseniz, muhtemelen veya öğesine parametre data olarak iletilen alana bakmışsınızdır ..send({ ... }) .sendTransaction({ ... })

Bu, calldata kısaca “bir mesaj çağrısı boyunca gönderilen verilerdir” (bir staticcall‘den, bir sözleşme çağrısından veya herhangi bir durum biçimini değiştiren gerçek bir işlemden bahsediyoruz – blockchain durumu veya sözleşme durumu, burada önemli değil).

Ethereum Sarı Döküman, calldata hakkında 21.sayfa 4.2. bölümde İşlem > Veriler ifadesini kullanmaktadır.

Çağrı verileri (calldata), EVM’deki özel bir veri konumudur. İki adres arasındaki herhangi bir mesaj çağrısı işlemi boyunca gönderilen ham onaltılık baytları ifade eder. EVM için, çağrı verilerinde bulunan herhangi bir veri, harici bir çağrı gerçekleştirmek için bir adres (EOA veya akıllı sözleşme) tarafından girdi olarak verildi.

Bir sözleşme çağrılırken (bir EOA’dan veya başka bir sözleşmeden), calldata, çağrılan işlevin ilk giriş parametrelerini (bağımsız değişkenler) içeren veri konumudur. Burası public ya da external fonksiyonlarının parametrelerinin saklandığı yerdir.

Diğer programlama dillerine bakıldığında, EVM’deki çağrı verileri aşağıdakilerle karşılaştırılabilir:

  • C++’daki dinamik bellek
  • C# dilinde yığın belleği

calldata Düzeni

Çağrı verileri, bellekle aynı şekilde sürekli olarak düzenlenen baytlardan oluşur. Bu, sözcüklerden (32 bayt uzunluğunda) oluşan depolama veya yığın gibi diğer veri konumlarının düzenine zıttır.

EVM’de çağrı verileri, EVM belleğine benzer şekilde bayt adreslenebilir bir alandır. Calldata‘da çeşitli türlerdeki değişkenlerin düzenlenme şekli, bellekte düzenlenmelerine çok benzer.

Okuma için, calldata bellekle aynı şekilde davranır: bir seferde 32 bayt yükleyebilirsiniz ( mload vs calldataload). Ancak, ona yazamadığınız için belleğe göre farklı davranır.

Calldata, herhangi bir EVM tabanlı blok zincirine çok özel bir veri konumudur ve belirli bir yerleşim düzenine sahiptir:

  • ilk 4 bayt, işlev imzasının seçicisine karşılık gelir.
  • kalan baytlar, işlevin giriş parametrelerine karşılık gelir. Her giriş bağımsız değişkeni her zaman 32 bayt uzunluğundadır. Türü 32 bayttan küçükse bağımsız değişken doldurulur.

Giriş bağımsız değişkenleri, türlerine bağlı olarak sağda veya solda doldurulur. Örneğin, uintN veya address solda, bytesN sağda doldurulur.

calldata’nın Temelleri

Calldata genellikle bellekle (memory) veya “bellekte belirli bir yer” ile karıştırılır. Calldata, ayrı bir veri konumu olduğu için bellekle aynı şey değildir. Hafıza ile farkını anlamak için amacını ama esas olarak nereden geldiğini anlamalıyız.

Calldata ile memory arasındaki farkı anlamak için sorulacak iyi bir soru, “calldata'da verileri kim oluşturur?” ve/veya “bellekte verileri kim oluşturur?” (“create” ve “allocate” kelimeleri burada birbirinin yerine kullanılabilir).

calldata ve memory arasındaki farkı ve bunların nasıl kullanılması gerektiğini öğrenmenin iyi bir yolu, calldata‘nın arayan (caller) tarafından, memory‘nin ise aranan (callee) tarafından tahsis edilmesidir.

  • caller (EOA veya Kaynak sözleşmesi olsun), Hedef sözleşmeye gönderilecek verileri oluşturan kişidir. Bu veriler çağrı verilerinde tahsis edilir ve bir mesaj çağrısı yoluyla Target’a gönderilir.
  • callee (Hedef sözleşmesi) çağrı verilerini tüketir ve belleği kullanarak daha fazla işlem yapar. İşlenmekte olan veriler, çağrı verilerinden veya kendi deposundan yüklenebilir.

Şimdi calldata‘nın ana özelliklerine bakalım. Calldata, bir işlemin veya aramanın data parametresinin tutulduğu bir veri konumudur.

  • Değiştirilemez (Salt okunur); çağrı verisindeki veriler değiştirilemez veya üzerine yazılamaz.
  • Boyut olarak neredeyse sınırsız; sabit bir sınır olmaksızın, boyut olarak neredeyse sınırsızdır.
  • Çok ucuz ve gaz açısından verimli; okuma ve çağrı verilerinde bayt tahsisi çok ucuz ve gaz açısından verimli.
  • Kalıcı olmayan (işlem tamamlandıktan sonra)
  • İşlemlere ve sözleşme çağrılarına özel.

Calldata değiştirilemez

Solidity‘deki calldata‘nın en önemli özelliklerinden birini anlayarak başlayalım.

Solidity‘deki calldata söz konusu olduğunda anlaşılması gereken en önemli kavramlardan biri “Calldata‘da depolanan veriler değişmez” oluşudur.

Harry Altman, “Data Representation in Solidity” başlıklı çok ayrıntılı makalesinde, karmaşık bir cümlenin ardındaki calldata hakkında önemli bir gerçeği belirtiyor:

“[…] bu nedenle, “calldata doğrudan değer türleri içeremez” gibi şeyler söyleyeceğiz, çünkü Solidity, değer türünde bir calldata değişkeni bildirmeye izin vermeyecektir (calldata‘daki orijinal değer, kullanımdan önce her zaman yığına kopyalanacaktır) . Açıkçası, değer calldata‘da hala var, ancak orada hiçbir değişken noktası olmadığından, bu bizi ilgilendirmiyor.”

Bizim için önemli olan kısım parantezler arasında: “calldata‘daki orijinal değer kullanılmadan önce her zaman yığına kopyalanacaktır.”

Bu alıntıdan üç şey çıkarabiliriz:

  1. calldata sabittir: içinde bulunan verileri değiştiremeyiz.
    Ve sonuç olarak
  2. calldata salt okunurdur
  3. calldata’dan değerler okunurken, değerler yığına kopyalanır.

Calldata‘nın değişmez olması, calldata anahtar sözcüğünü kullanarak yalnızca Solidity‘deki bir referans aracılığıyla calldata‘ya erişebileceğimiz gerçeğine de yol açar.

Veri konumu olarak calldata ile belirtilen karmaşık türdeki herhangi bir değişken salt okunurdur. Değişken değiştirilemez. Bu, değişken bir işlev parametresi olarak iletilirse veya işlev gövdesi içinde tanımlanırsa geçerlidir.

Aşağıdaki Solidity kod parçacıkları ile pratikte görelim. Bu kodu Remix‘e yapıştırırsanız, Solidity derleyicisi calldata olarak belirtilen girdi değişkenlerini düzenlemenize izin vermeyeceğinden şikayet eder.

pragma solidity ^0.8.7;

contract AllAboutCalldata {

    function manipulateMemory(string memory input) public pure returns (string memory) {
        // veri konumu 'bellek' ile iletilen argümanları değiştirebilirsiniz

        // diziye veri ekleyebilirsiniz
        input = string.concat(input, " - All About Solidity");

        // tüm dizeyi değiştirebilirsiniz
        input = "Changed to -> All About Memory!";
        return input;
    }

    function manipulateCalldata(string calldata input) external pure returns (string calldata) {
        // 'calldata' veri konumuyla iletilen bağımsız değişkenleri DEĞİŞTİREMEZSİNİZ

        // diziye veri ekleyemez veya düzenleyemezsiniz
        // TypeError: Tür dizesi belleği, dolaylı olarak beklenen tür dizesi calldata'ya dönüştürülemez.
        input = string.concat(input, " - All About Solidity");

        // tüm diziyi değiştiremezsin
        // Literal_string "..." türü, dolaylı olarak beklenen tür dizesi calldata'ya dönüştürülemez.
        input = "Cannot change to -> All About Calldata!";
        return input;
    }
}

Calldata‘nın boyutu neredeyse sınırsızdır.

Calldata, belleğe göre ek bir avantaj sunar: boyutu.

Belleğin maksimum boyut sınırı vardır. 2 ** 64 bayta kadar tutabilir (bir uint64‘ün maksimum değeri).

Karşılaştırıldığında, çağrı verilerinin boyutu neredeyse sınırsızdır. Bu hem sarı belgede açıklanmıştır hem de geth istemci kaynak kodundaki türünden anlaşılabilir.

// İşlem bir Ethereum işlemidir.
type Transaction struct {
	inner TxData    // Bir işlemin mutabakat içeriği
	time  time.Time // Yerel olarak ilk görülme zamanı (spam engelleme)

	// önbellekler
	hash atomic.Value
	size atomic.Value
	from atomic.Value
}

Kaynak: geth istemci kaynak kodu (Github) — core/types/transaction.go, satır 50–59

// TxData, bir işlemin altında yatan verilerdir.
//
// Bu, DynamicFeeTx, LegacyTx ve AccessListTx tarafından uygulanır.
type TxData interface {
	txType() byte // tür kimliğini döndürür
	copy() TxData // derin bir kopya oluşturur ve tüm alanları başlatır

	chainID() *big.Int
	accessList() AccessList
	data() []byte
	gas() uint64
	gasPrice() *big.Int
	gasTipCap() *big.Int
	gasFeeCap() *big.Int
	value() *big.Int
	nonce() uint64
	to() *common.Address

	rawSignatureValues() (v, r, s *big.Int)
	setSignatureValues(chainID, v, r, s *big.Int)
}

Kaynak: geth istemci kaynak kodu (Github) — core/types/transaction.go, satır 77

Bu, bir dereceye kadar “calldata‘nın gerektiği kadar bayt tutabileceği” anlamına gelir. Ancak teknik olarak hafıza gibi çağrı verileri de blok gaz limitine bağlı olacaktır.

Bununla birlikte, çağrı verilerinde daha fazla bayt ayırmanın maliyeti, bellek boyutu büyüdükçe karesel olarak giden belleğe kıyasla her zaman doğrusaldır. Bu farkı bir sonraki alt bölümde göreceğiz.

Calldata çok ucuz ve gaz açısından verimli

Calldata salt okunur olmasına ve ona yazamamanıza rağmen yine de bir maliyeti vardır. Ancak bu maliyet, diğer veri konumlarına kıyasla gaz açısından nispeten ucuzdur.

Calldata‘nın her baytının bir maliyeti vardır:

  • Sıfır bayt 0x00 için 4 gaz
  • Sıfır olmayan baytlar için 16 gaz.

Sıfır olmayan baytlar için gaz maliyeti, EIP 2028 — İşlem Verileri Gaz Maliyeti azaltma ile değiştirilmiştir. Calldata’nın gaz maliyetini düşürmenin amacı, zincir üzerinde ölçeklenebilirliği artırmaktı. Calldata daha ucuz olacağından, her bir işlemin içine daha fazla calldata baytı sığabilir ve genel olarak daha fazla veri tek bir bloğa sığabilir (EIP’nin yazarının “çağrı verilerinin daha yüksek bant genişliği” dediği şey).

EIP 2028, Katman 2 Ölçeklenebilirlik çözümlerini teşvik eder. Bir soğanın katmanları gibi, gaz maliyetli işlemler (depolama okuma/yazma + hesaplama) dış katmanda (zincir dışı) taşınır ve bunun yerine veri iletimini sağlar. Bu, kanıt sistemi/sahtekarlık kanıtları (birden çok işlemi tek bir kanıt tx’te toplu olarak toplayın) veya verileri çağrı verileri yoluyla ana zincire yerleştirin.

Benzer şekilde, CALLDATALOAD işlem kodunun, onu calldata‘dan yığına yükleyerek 32 baytlık bir kelimeyi calldata‘dan okuması yalnızca 3 gaza mal olur.

Buna karşılık, MLOAD işlem kodunu kullanarak bellekten okuma maliyeti, belleğin mevcut boyutuna ve bellek genişletme maliyetine bağlıdır.

Calldata, harici aramalara özeldir – Gnosis ile Örneklem

Açıklandığı gibi, çağrı verileri çoğunlukla harici işlemlerden (EOA’lar) veya sözleşme çağrılarından elde edilen verileri ifade eder. Yalnızca mesaj aramalarına özgüdür ve sözleşme oluşturma işlemlerine özgü değildir (bu farkı aşağıda “Constructor’da calldata” bölümünde göreceğiz.

Popüler bir projenin Solidity kodundan örnek kullanalım. Örnek kod kaynağımız “Gnosis-Safe”.

Bir Gnosis-Safe, kasanın depolanmasını başlatan bir setUp(…) işlevine sahiptir. İlk parametrenin, kasanın sahibi olan sahip adresess[] dizisi olduğunu görebilirsiniz. İşlev external olarak tanımlandığından, bunlar veri konumu calldata ile belirtilir.

/// @dev Kurulum işlevi, sözleşmenin ilk depolanmasını ayarlar.
 /// @param _owners Güvenli sahipler listesi.
 /// @param _threshold Güvenli işlem için gerekli onayların sayısı.
 /// İsteğe bağlı temsilci çağrısı için Sözleşme adresine @param.
 /// @param data İsteğe bağlı temsilci çağrısı için veri yükü.
 /// Bu sözleşmeye geri dönüş çağrıları için @param fallbackHandler İşleyici
 /// Ödeme için kullanılması gereken @param PaymentToken Token (0, ETH'dir)
 /// @param ödeme Ödenmesi gereken değer
 /// Ödemeyi alması gereken @param PaymentReceiver Adresi (veya tx.origin ise 0)

    function setup(
        address[] calldata _owners,
        uint256 _threshold,
        address to,
        bytes calldata data,
        address fallbackHandler,
        address paymentToken,
        uint256 payment,
        address payable paymentReceiver
    ) external {
        // setupOwners, Threshold'un önceden ayarlanmış olup olmadığını kontrol eder, bu nedenle bu yöntemin iki kez çağrılmasını engeller
        setupOwners(_owners, _threshold);
        if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
        // setupOwners yalnızca sözleşme başlatılmamışsa çağrılabileceğinden, setupModules için bir kontrole ihtiyacımız yoktur.
        setupModules(to, data);

        if (payment > 0) {
            // EIP-170 ile ilgili sorunlarla karşılaşmamak için, handlePayment işlevini yeniden kullanıyoruz (doğrulanmış kodun ayarlanmasını önlemek için yöntemin kendisini ayarlamıyoruz)
            // baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
            handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
        }
        emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
    }

Kaynak : GnosisSafe.sol

Solidity’de çağrı verilerine erişme

Sarı Kâğıda bir saniye bakalım. Yürütme ortamı olarak tanımlanan çağrı verileri aşağıdaki alanları içerir:

Kaynak: Ethereum Yellow Paper, sayfa 13, bölüm 9.3

Bu tür alanlara Solidity‘de msg global değişkeni aracılığıyla erişilebilir. . Bu global değişken, aşağıdakiler de dahil olmak üzere çağrı verilerinden farklı bilgilere erişim sağlar:

msg.sender –> mesaj çağrısını başlatan adres.
msg.value –> çağrıda veya işlemde gönderilen değer.
msg.sig –> işlev tanımlayıcısı (işlev imzasının keccak256 karmasının ilk 4 baytı). Bu, çağrı verilerinin ilk 4 baytına erişmeye eşdeğerdir. (bir örnek göster)
msg.data –> açıklama gerekli
gasleft() –> işlemde kalan kalan gaz miktarı.

Solidity‘nin 0.6.4 sürümüne kadar, veri konumu calldata yalnızca harici işlev çağrılarının parametresi için mevcuttu. Solidity 0.6.4’ten bu yana, genel, dahili veya özel işlevlerin parametresi için bir veri konumu olarak calldata belirtilebilir. gasleft() eskiden msg.gas olarak biliniyordu ancak 0.5.0’da kaldırıldı.

msg.sig

Çağrı verilerinin ilk 4 baytı, işlev seçiciye atıfta bulunur. Çağrılan işlevin seçicisidir.

Fonksiyon imzasının keccak256 hash’inin ilk 4 byte’ı alınarak elde edilebilir. Örneğin, aşağıdaki işlev için:

function withdraw(uint256a amount) external {}

İşlev imzası, parantezler arasında parametre türleri bulunan işlev adı olarak türetilebilir.

withdraw(uint256)

İşlev imzasının hash edilmesi aşağıdakilerle sonuçlanacaktır:

keccak256("withdraw(uint256)") = 0x2e1a7d4d1332***a9021cf91d15***5b69f16a49f

İşlev seçici, 0x2e1a7d4d olan yalnızca ilk 4 bayta karşılık gelir.

İşlev birden fazla bağımsız değişken içeriyorsa, işlev imzasındaki hash parametre türleri, boşluksuz virgül “,” ile ayrılmış parametreler OLMALIDIR. Örneğin, ERC20’nin popüler transfer işlevi.

// işlev tanımı
function transfer(address to, uint256 amount) public returns (bool)
// imza
transfer(address,uint256)
// keccak256("transfer(address,uint256)")
0xa9059cbb2ab09eb2****4a59a5d0623ade3****cd4e46b11d***9049b
selector = 0xa9059cbb

Son olarak, işlev parametreleri karmaşık veya dinamik tipler olduğunda ve bir veri konumu belirttiğinde, işlev imzası hiçbir zaman veri konumunu içermez. Örneğin, ERC721‘deki popüler safeTransferFrom işlevi, 4. parametre bytes memory data‘yı içerir.

// işlev tanımı
function safeTransferFrom(
    address from,        
    address to,        
    uint256 tokenId,        
    bytes memory data    
) public
// imza
safeTransferFrom(address,address,uint256,bytes)
// keccak256("safeTransferFrom(address,address,uint256,bytes)")
0xb88d4fde60196325a28bb7f99a2582e0b46de55b18761e960c14ad7a32099465
selector = 0xb88d4fde

EVM, bir aramada hangi işlevin yürütülmesi gerektiğini belirlemek için arama verilerinin seçicisini kullanır.

Bu nedenle fonksiyon seçiciye msg.sig global üyesini kullanarak erişebilirsiniz.

Calldata dilimlerini kullanarak aşağıdaki gibi de çıkarabilirsiniz.

bytes4 selector = msg.data[4:]

Not: yapıcılar için msg.sig

Kurucularda oluşturma kodu çalıştığında 4 bayt ofset yoktur. Bunun nedeni, oluşturucularda calldata‘nın boş olmasıdır (msg.sig özel değişkeni 4 sıfır bayt içerecek şekilde doldurulur). Bunu aşağıda daha ayrıntılı olarak göreceğiz.

msg.data

Global msg.data, işlev seçici de dahil olmak üzere tüm çağrı verilerine erişmenizi sağlar.

Çağrı verisi, daha önce gördüğümüz işlev seçiciden (4 bayt) + işleve iletilen argümanların geri kalanından (varsa) oluşur.

Çağrı verileri bağlamında, bu bağımsız değişkenlere daha genel olarak girdi verileri denir.

Akılda tutulması gereken bir şey, girdi verisi bağımsız değişkenlerinin 32 bayt uzunluğunda sözcükler olduğudur. Bir işleve geçmesi gereken herhangi bir argüman 32 baytlık parçalar halinde eklenebilir.

Bunlar “kelimeler” olarak adlandırılır ve işlemin girdi verilerinde imza karmasını takip eder.

Assembly ile Calldata işlem kodları

EVM, “calldata” çağrı verileriyle etkileşime geçmek için üç ana işlem kodu sağlar.

CALLDATALOAD : parametre olarak verilen belirli bir ofsetten başlayarak çağrı verisinden 32 baytlık bir kelimenin yüklenmesine izin verir.
CALLDATASIZE : çağrı verilerinde bulunan bayt sayısını döndürür
CALLDATACOPY : belirli sayıda baytı çağrı verisindeki bir kaynaktan bellekteki belirli bir hedefe kopyalayın.

Solidity, bu işlem kodlarının satır içi derleme sürümünü sağlar.

Onları daha iyi anlamak için, Gnosis Safe sözleşmelerinden bir örneği yeniden kullanalım. GnosisSafeProxy.sol (genel bir proxy sözleşmesi), calldata‘yı bir veri konumu olarak anlamak için kullanılacak bir örnektir.

Bu tekil proxy yalnızca bir işlev içerir: fallback() işlevi. Sözleşmeye yapılan herhangi bir çağrı bu işlevde sona erer. fallback() işlevi daha sonra calldata‘yı alır ve bir delegatecall aracılığıyla uygulama sözleşmesine (singleton değişkeni tarafından tanımlanır) gönderir. Bu bir delegatecall olduğu için, işlevin mantığı proxy sözleşmesi bağlamında çalışır (delegatecall‘ı yapar) ve güncellenecek olan proxy sözleşmesinin deposu/durumudur.

Delegatecall, bellekteki verilerle çalışır. Bu nedenle, fallback işlevinin çalışması için önce tüm çağrı verilerinin belleğe kopyalanması gerekir.

Delegatecall yapısının nasıl çalıştığı hakkında daha fazla ayrıntı için “Adresler Hakkında Her Şey” başlıklı makaleye bakın.

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

/// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain
/// @author Richard Meissner - <[email protected]>
interface IProxy {
    function masterCopy() external view returns (address);
}

/// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract.
contract GnosisSafeProxy {
    // singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated.
    // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt`
    address internal singleton;

    /// @dev Constructor function sets address of singleton contract.
    /// @param _singleton Singleton address.
    constructor(address _singleton) {
        require(_singleton != address(0), "Invalid singleton address provided");
        singleton = _singleton;
    }

    /// @dev Fallback function forwards all transactions and returns all received return data.
    fallback() external payable {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
            // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
            if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
                mstore(0, _singleton)
                return(0, 0x20)
            }
            calldatacopy(0, 0, calldatasize())
            let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            if eq(success, 0) {
                revert(0, returndatasize())
            }
            return(0, returndatasize())
        }
    }
}

Bu kod parçacığındaki fallback() işlevi, calldata ile ilgili üç işlem kodunu kullanır.

calldataload

calldataload işlem kodu, işlem verilerinin 32 baytını yığına yükler. Aşağıda gösterildiği gibi yalnızca bir argüman alır:

calldataload(startingOffset)

Yüklemenin başlayacağı calldata‘daki startupOffset (index).

Ardından, calldata‘da belirtilen ofsetten başlayarak yığına 32 bayt yükler.

Gnosis örneğinde, calldataload, calldata‘nın başından başlayarak 32 bayt yükler (ofset 0). Ardından, calldata‘nın ilk 4 baytının masterCopy() işlev imzasının karmasına karşılık gelip gelmediğini karşılaştırır.

Karşılaştırma, değişmez değeri 0xa619486e‘yi 32 bayta kadar yapmak için bazı sıfırlar 0000… ile doldurarak yapılır. Her iki değer (değişmez hex dizesi ve calldata‘dan yüklenenler) eşitlik işlem kodu eq() aracılığıyla karşılaştırılır.

Bir eşleşme varsa (calldata, masterCopy() işlevinin işlev seçicisidir), sözleşme çağrısı yapılmaz. masterCopy() işlevi, uygulama sözleşmesinin adresini döndürür. Bu nedenle, bir sözleşme çağrısı yapmaktan kaçınmak için (bu, maliyetli olacak ve gereksiz yere gaz israfına neden olacaktır), _singleton değişkenini (uygulama adresini içeren) döndürürüz.

calldatacopy

calldatacopy, işlem verilerinin belirli sayıda baytını belleğe kopyalar. calldata işlem kodu üç bağımsız değişken alır:

calldatacopy(
    memoryDestinationOffset, 
    calldataStartOffset, 
    nbOfBytes
)
  • Sonucun kopyalanacağı bellekteki memoryDestinationOffset.
  • Kopyalamaya başlamak için calldata‘daki calldataStartOffset.
  • Kopyalanacak ise nbOfBytes.

Gnosis örneğinde, bu işlem kodu, tüm çağrı verilerini bir bellek işaretçisine (sözleşmeye gönderilen tüm veri yükü) kopyalamak için geri dönüş işlevinde kullanılır. Tüm yükü belleğe kopyaladıktan sonra bir sonraki satırda delegatecall tarafından tüketilir.

GnosisSafeProxy sözleşmesi tüm çağrı verilerini nasıl yükler? calldatacopy işlem kodunun üçüncü parametresine bakarsanız, bu parametre için kullanılan değerin kendisinin bir işlem kodu olduğunu göreceksiniz.

calldatasize

calldatasize işlem kodu, işlem verilerinin boyutunu bayt cinsinden döndürür. Sonucu yığının üstüne çıkarır. Herhangi bir argüman almaz.

calldatasize()

Bir önceki bölümde açıklandığı gibi, calldata‘nın tamamını, delegatecall işlem kodu için tüketilebilmesi için belleğe yüklemek için kullanılır.

Düşük Seviye Aramalar için calldata Oluşturma

tx/message çağrısının veri alanında bulunanların, akıllı bir sözleşmenin kodun hangi bölümünün yürütüleceğini ve çalıştırılacağını bilmesini sağladığını öğrendik. Calldata olarak adlandırdığımız bu veri alanı, çağrılacak işlevin bytes4 işlev seçicisini ve argümanlarını içeren bir bayt dizisidir. Her zaman calldata‘nın tamamını (bytes4 seçici ve işlevler argümanları) bir yük olarak adlandırırım (kötü amaçlı yazılım teriminde değil).

Wikipedia ‘ya göre; Yük, asıl amaçlanan mesaj olan iletilen verilerin parçasıdır. Başlıklar ve meta veriler yalnızca yük teslimatını etkinleştirmek için gönderilir.

address(target_contract).call(calldataPayload) aracılığıyla harici düşük seviyeli çağrılar yaparken, tx/msg çağrısında iletilecek çağrı verilerini oluşturmanın birçok yolu vardır.

Solidity‘de bu tür çağrı verilerini bir bayt değeri olarak oluşturmak için mevcut tüm seçenekleri aşağıda göreceğiz, böylece düşük seviyeli bir .call() aracılığıyla gönderilebilir. Bu, yeni kullanıma sunulan abi.encodeCall(…) öğesini içerecektir.

Şu örnek senaryoyu hayal edin: Bir CallerContract, bir DeployedContract ile etkileşime girer. CallerContract, DeployedContract‘ta add(uint256) işlevini çağırmayı amaçlar.

// SPDX-LICENSE: UNLICENSED
pragma solidity ^0.8.0;

contract DeployedContract {
    uint public result = 0;

    function add(uint256 input) public {
        result = result + input;
    }
}

contract CallerContract {    
    DeployedContract deployed_contract;

    constructor(DeployedContract deployedContract_) {
        deployed_contract = deployedContract_;
    }

    // aşağıdaki farklı türlerdeki örneklere bakın
    // low level call

}

değişmez bir dize olarak calldata

function callWithLiteralString() public {
    bytes memory calldataPayload = "0x1003e2d200000000000000******00000000005";
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

Calldata değişmez onaltılık ”…” dizesi olarak

Bu, önceki örnekten ince bir farktır.

function callWithLiteralHexString() public {
    bytes memory calldataPayload = hex"1003e2d200000000000*****000000000000000005";
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

bytes4 seçiciyi keccak256 ile manuel olarak oluşturarak veri çağırın

Aşağıda, işlev seçiciyi ABI tarafından belirtilen kurallara göre manuel olarak hesaplıyoruz. Aşağıdakileri yapıyoruz:

  • keccak256 ve işlev imzasını hashleyin, yalnızca ilk 4 baytı saklayın.
  • işlev çağrısının parametrelerini sonuna ekleyin.

Ekleme/birleştirme için, herhangi bir Solidity türünü ABI spesifikasyonlarına göre düşük seviyeli bayt gösterimine dönüştürmek için abi.encodePacked(…) kullanıyoruz. (bytes.concat(…) yalnızca sabit boyutlu baytları kabul edebildiği için burada bunu yapmamıza olanak sağlamaz).

function callWithFunctionSignatureFromHash(uint256 input) public {
    bytes memory calldataPayload = abi.encodePacked(
        bytes4(keccak256("add(uint256)")),
        input
    );

    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

abi.encodeWithSignature kullanarak Calldata

function callWithEncodeWithSignature(uint256 input) public {
    bytes memory calldataPayload = abi.encodeWithSignature("add(uint256)", input);
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

abi.encodeWithSelector ve bayt4 değeri kullanarak Calldata

Bu örnekte, işlev seçici, bytes4 sabit değeri olarak manuel olarak yazılmıştır.

function callWithEncodeWithSelectorAsLiteral(uint256 input) public {
    bytes memory calldataPayload = abi.encodeWithSelector(0x1**3e2d2, input);
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

byte4 seçiciyi aşağıdakiler aracılığıyla da oluşturabilirsiniz:

bytes4(keccak256("add(uint256)"))

abi.encodeWithSelector ve functionName.selector kullanarak Calldata

İşlev seçicileri manuel olarak hesaplamak veya oluşturmak zor ve hataya açık olabilir. Aslında, örneğin abi.encodeWithSignature(…) için sağlanan dizgedeki bir yazım hatası, yanlış seçiciyi oluşturabilir.

Yerleşik Solidity üyesi .selector‘ı kullanarak seçiciyi işlevden çıkarabilirsiniz. Bir sözleşmenin her işlevi bu özelliğe sahiptir (sözleşme –contract– ve arayüz –interface– dahil).

function callWithEncodeWithSelectorAsReference(uint256 input) public {
    bytes memory calldataPayload = abi.encodeWithSelector(deployed_contract.add.selector, input);
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

abi.encodeCall kullanarak Calldata

Bu yeni sözdizimi Solidity 0.8.11’de tanıtıldı. Sağlanan parametrelere tip güvenlik kontrolleri ekler. Tek fark, işlev seçici konseptinden uzaklaşıyor olmamız, bunun yerine işlev işaretçileri kullanıyor olmamızdır.

function callWithABIEncodeCall(uint input) public { 

    function (uint256) external functionToCall = deployed_contract.add;

    bytes memory calldataPayload = abi.encodeCall(functionToCall, input);
    (bool success, ) = address(deployed_contract).call(calldataPayload);
}

Calldata’dan Dilimleri Çıkarma

data[start:end] sözdizimi yalnızca calldata‘ya işaret eden bayt değişkenleri için kullanılabilir, memory için kullanılamaz.

Calldata‘nın sürekli bir bayt dizisi olduğunu öğrendik. Calldata dilimlerini kullanarak Solidity‘de calldata‘nın bir kısmını çıkarabilirsiniz. Bu özellik, Solidity‘nin 0.6.0 ana sürümüyle kullanıma sunuldu.

Calldata dilimleri, calldata‘nın dilimlerini almanızı sağlar. Döndürülen dilim bytes türündedir. Aşağıdakileri belirterek çalışır:

  • dilimlemeye başlamak için ofset.
  • dilimlemenin nerede sonlandırılacağını belirlemeye yardımcı olan ofset.

İki nokta üst üste : hem başlangıcı hem de bitişi ayırır. Başlangıcı veya sonu da yayınlayabilirsiniz. Başlangıcın atlanması, dilimlemenin 0 ofsetinde başlatılması için varsayılan olacaktır. End varsayılanının atlanması, çağrı verilerindeki son bayta kadar dilimleme için varsayılandır.

Dilimlemenin kullanılabileceği ve yararlı olabileceği iki durum vardır.

  • Doğrudan tüm çağrı verisinden dilimleme — msg.data.
msg.data[start:end]
  • Veri konumu calldata ile bir baytı dilimleyin.
function example(bytes calldata input) public {
    bytes calldata secondThirdBytes = input[1:3];
}

Epey anlattık, biraz örneklerle açıklayalım.

İşlev seçiciyi ayıklayın

İşlevin imzası, çağrı verilerinin ilk 4 baytını dilimleyerek çıkarılabilir. Bu, msg.sig‘e eşdeğerdir (msg.sig‘in ilk 4 bayta otomatik olarak dönüştürülmesi dışında. )

bytes4 selector = msg.data[:4];

Not: bayttan baytN‘ye dönüştürme, Solidity 0.8.5’ten beri mevcuttur.

Dahili Fonksiyonlarda Calldata

Calldata‘yı bağımsız değişken olarak alan dahili bir işleve neden bir memory değişkeni iletemezsiniz?

Fakat neden tam tersini yapabilirsiniz: argüman olarak memory alan dahili bir işleve bir calldata değişkeni iletebilirsiniz?

Cevap, calldata ve memory‘nin özelliklerinde yatmaktadır. Gördüğümüz gibi, calldata salt okunurdur. Aksine, memory okunabilir ve yazılabilir bir veri yeridir.

Bir calldata başvurusu, memory‘yi işlev bağımsız değişkeni olarak kabul eden dahili bir işleve iletildiğinde, EVM bu değeri calldata‘dan memory‘ye iki şekilde taşıyabilir:

  • calldataload (değeri calldata’dan stacl’a yüklemek için) ve ardından mstore (yüklenen değeri belleğe yazmak için) işlem kodlarını kullanarak.
  • değeri calldata‘dan doğrudan belleğe tek bir adımda taşımak için opcode calldatacopy’yi kullanarak.

Gerçekte değerler, kelimenin tam anlamıyla calldata‘dan “taşınmaz”. Daha çok kopyalanırlar. Calldataload ve calldatacopy işlem kodlarının her ikisi de kopyalar oluşturur. calldata‘daki veriler sabittir ve yalnızca kopyaları yapılır.

  • EVM bunu yapabilir: calldata → ✅ memory
  • EVM bunu yapamaz: calldata ❌ ← memory

Bu, calldata‘nın yalnızca tek yöne giden bir veri konumu olduğunu görmemizi sağlar: yalnızca calldata‘dan gelen değerler yüklenebilir. Bu nedenle, calldata‘dan alınan değerler her zaman:

  • calldataload veya calldatacopy kullanılarak yığına yüklenir (kopyalanır).
  • yüklenen/kopyalanan değerler daha sonra depolamaya, belleğe veya koda taşınır.

Constructor ‘da Calldata

Calldata’nın farklı davrandığı özel bir durum vardır. Yapıcı ile durum budur.

Bir oluşturucu bağlamında, veri konumu calldata, bir işlev parametresi olarak mevcut değildir.

Bunun nedeni, bir sözleşme dağıtıldığında ve yapıcı çalıştığında calldata‘nın her zaman boş olmasıdır. Bir dağıtım işlemi sırasında, sözleşme oluşturma kodu veri alanına (calldata) girmez. Bunun yerine sözleşme oluşturma kodu, işlemin başlangıç alanına gider.

Bu durum sarı kağıtta açıklanmaktadır. Veri alanı, yalnızca içinde bazı kodlar depolanmış adreslere yapılabilen mesaj çağrıları için kullanılır. Yapıcı çalıştığında, adres bilinebilir, ancak dağıtılmakta olan sözleşmenin adresi altında hiçbir bayt kodu depolanmaz.

Sonuç olarak, bir oluşturucu bağlamındaki çağrı verilerinde 4 bayt ofset (bytes4 işlev seçici) yoktur. msg.sig özel değişkeni 4 sıfır bayt içerecek şekilde doldurulur.

Solidity Programlama Dili Nedir?

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çeirğ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şlayacğaım diyenler için Blockchain ​​Developer Olmak İçin Yol Haritası içeriğine de muhakkak bakın.

Gelin aklınızdaki soruları SUPERPEER sobetinde cevaplayalım.

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