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:
- calldata sabittir: içinde bulunan verileri değiştiremeyiz.
Ve sonuç olarak - calldata salt okunurdur
- 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:
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ürCALLDATACOPY
: 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.
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 Twitter, Linkedin ve YouTube‘da takip edin.
Kısa bir yorum bırakmayı UNUTMAYIN!