Tutorial·8 min का पठन·Solingo द्वारा

Transient Storage (EIP-1153): tstore और tload व्यवहार में

Transient storage, EVM को एक सस्ती, transaction-scoped memory देता है जो अपने आप reset हो जाती है। जानिए TSTORE और TLOAD कैसे काम करते हैं, और एक ऐसा reentrancy guard बनाइए जो storage-based version की एक छोटी सी fraction खर्च करता है।

# Transient Storage (EIP-1153): tstore और tload व्यवहार में

हर Solidity developer SSTORE का दर्द जानता है। एक single storage slot को zero से लिखने में 20,000 gas तक लग सकता है, और एक non-zero slot को update करने में भी 5,000 gas लगता है। लेकिन इनमें से बहुत सा data को current transaction के बाद जीवित रहने की ज़रूरत ही नहीं होती। EIP-1153 द्वारा पेश किया गया transient storage आपको एक दूसरा key-value store देता है जो सिर्फ एक transaction के समय तक जीवित रहता है, और फिर बिना किसी cost के गायब हो जाता है। यह article दोनों नए opcodes समझाता है, एक ठोस reentrancy guard बनाता है, gas की तुलना करता है, और production में भेजने से पहले समझने योग्य caveats की सूची देता है।

Transient storage असल में क्या है

Transient storage, EVM के अंदर एक अलग address space है, जो सामान्य (persistent) storage से अलग है। यह ठीक persistent storage जैसा ही organized है: 256-bit keys से 256-bit values की एक mapping, हर contract address के लिए अलग। निर्णायक अंतर इसकी lifetime में है।

  • Persistent storage को SSTORE से लिखा जाता है, SLOAD से पढ़ा जाता है, और यह transactions के बीच तब तक जीवित रहता है जब तक आप इसे overwrite न करें।
  • Transient storage को TSTORE से लिखा जाता है, TLOAD से पढ़ा जाता है, और हर transaction के अंत में यह अपने आप zero पर wipe हो जाता है।

दोनों opcodes Cancun upgrade में जोड़े गए थे:

  • TLOAD (opcode 0x5c): stack से एक key pop करता है, stored value push करता है।
  • TSTORE (opcode 0x5d): stack से एक key और एक value pop करता है, value लिखता है।

दोनों flat-priced हैं। एक TLOAD और एक TSTORE, हर एक 100 gas खर्च करता है, बिल्कुल warm storage access जितना, zero बनाम non-zero values या refunds के किसी surprise के बिना। यही predictability transient storage को आकर्षक बनाने वाली बातों में से एक है।

एक transaction के भीतर lifetime

यह सबसे महत्वपूर्ण property है, इसलिए इसे दो बार पढ़िए। Transient storage transaction के अंत में clear होता है, किसी single call frame के अंत में नहीं।

इसका मतलब है कि जो value आप एक external call में TSTORE करते हैं, वह उसी transaction के किसी बाद वाले call में TLOAD से अब भी पढ़ी जा सकती है। अगर contract A, contract B को call करता है जो वापस A में call करता है, तो A के पहले call की शुरुआत में लिखा गया transient slot reentrant call आने पर भी set रहता है। ठीक यही behaviour एक reentrancy guard को संभव बनाता है।

सीमाएं याद रखिए:

  • दो अलग transactions के बीच: transient storage हमेशा zero से शुरू होता है।
  • एक transaction के भीतर nested calls में: values बनी रहती हैं।
  • revert पर: transient storage के बदलाव roll back हो जाते हैं, ठीक persistent storage की तरह, इसलिए एक reverted sub-call slot को उसी value पर छोड़ देता है जो उस sub-call से पहले थी।

एक reentrancy guard बनाना

Classic OpenZeppelin ReentrancyGuard एक persistent storage slot का उपयोग करता है जिसे दो states के बीच toggle किया जाता है। यह हर protected function के entry पर एक SSTORE और exit पर एक SSTORE देता है। Transient storage वही काम कहीं कम में कर देता है।

Solidity 0.8.24 और उसके बाद आप सीधे inline assembly उपयोग कर सकते हैं। Opcodes tstore और tload के रूप में उपलब्ध हैं:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

abstract contract TransientReentrancyGuard {

// lock flag के लिए एक fixed, arbitrary slot.

// keccak256("transient.reentrancy.guard") - 1 इसे सामान्य storage

// layout से दूर रखता है, भले ही transient space अलग हो.

bytes32 private constant LOCK_SLOT =

0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;

error ReentrantCall();

modifier nonReentrant() {

_checkAndLock();

_;

_unlock();

}

function _checkAndLock() private {

assembly {

if tload(LOCK_SLOT) {

// ReentrantCall() के साथ revert

mstore(0x00, 0xab143c06)

revert(0x1c, 0x04)

}

tstore(LOCK_SLOT, 1)

}

}

function _unlock() private {

assembly {

tstore(LOCK_SLOT, 0)

}

}

}

इसका उपयोग persistent version जैसा ही है:

contract Vault is TransientReentrancyGuard {

mapping(address => uint256) public balances;

function withdraw(uint256 amount) external nonReentrant {

require(balances[msg.sender] >= amount, "insufficient");

balances[msg.sender] -= amount;

(bool ok, ) = msg.sender.call{value: amount}("");

require(ok, "transfer failed");

}

}

चूंकि lock tstore से set होता है और transaction-scoped lifetime इसे reentrant call के पार set रखती है, इसलिए withdraw में वापस call करने वाला एक malicious receiver tload(LOCK_SLOT) != 0 पर पहुँचता है और revert हो जाता है। जब outer call खत्म होता है, _unlock slot को clear कर देता है, और transaction के अंत का automatic wipe एक safety net है, भले ही आप भूल जाएं।

Higher-level syntax

अगर आप किसी recent compiler पर हैं, तो आप एक state variable को transient declare कर सकते हैं और assembly पूरी तरह छोड़ सकते हैं:

pragma solidity ^0.8.28;

abstract contract TransientGuard {

bool transient locked;

error ReentrantCall();

modifier nonReentrant() {

if (locked) revert ReentrantCall();

locked = true;

_;

locked = false;

}

}

transient data location उन्हीं TSTORE और TLOAD opcodes में compile होती है, लेकिन compiler आपके लिए slot manage करता है। एक transient variable को पढ़ना और लिखना type-safe है और हाथ से लिखी assembly की तुलना में कहीं कम error-prone है।

SSTORE के मुकाबले gas savings

मुख्य आंकड़ा सरल है। Persistent storage पर आधारित एक guard, सामान्य warm case में, lock set करने के लिए लगभग 5,000 gas और इसे reset करने के लिए थोड़ा कम देता है (reset पर एक storage refund लागू होता है, लेकिन refunds per transaction capped होते हैं और कभी पूरी cost वापस नहीं करते)। एक transient guard tstore के लिए flat 100 gas और tload के लिए 100 gas देता है।

| Operation | Persistent storage | Transient storage |

| --- | --- | --- |

| एक fresh slot लिखना | 20,000 gas | 100 gas |

| एक warm slot update करना | 5,000 gas | 100 gas |

| एक warm slot पढ़ना | 100 gas | 100 gas |

| Tx के अंत में cleanup | manual reset, partial refund | automatic, free |

एक reentrancy guard के लिए practical saving प्रति protected call कुछ हज़ार gas के क्रम में होती है। वह win उन patterns के लिए और बड़ी होती है जो पहले एक ही transaction के भीतर persistent storage को scratchpad की तरह दुरुपयोग करते थे, जैसे किसी AMM में flash-accounting, flash loans के लिए callback context, या calldata को छुए बिना calls के बीच data pass करना।

Reentrancy से परे असली use cases

  • Flash accounting: एक DEX, multi-step swap के दौरान token deltas को transient storage में record कर सकता है और transaction खत्म होने से पहले उन्हें net zero पर होने की मांग कर सकता है, जिससे बार-बार SSTORE writes से बचा जा सके।
  • Reusable callback context: एक flash loan provider, borrower और अपेक्षित repayment को transient slots में रख सकता है, फिर सब कुछ calldata में encode किए बिना callback में उन्हें वापस पढ़ सकता है।
  • Per-transaction caches: कोई भी value जो recompute करने में महंगी है और सिर्फ एक transaction के भीतर चाहिए, वह उस memory के बजाय transient storage में रह सकती है जो call frames के पार नहीं जाती।

जिन caveats का पालन ज़रूरी है

  • Composability risk: चूंकि transient storage एक transaction के भीतर calls के पार बना रहता है, जो contract अपने slots clear नहीं करता वह उसी transaction के बाद वाले calls में state leak कर सकता है। transaction-end wipe होने के बावजूद, जो आप set करें उसे हमेशा reset करें।
  • Reverts, transient writes को roll back करते हैं: यह आमतौर पर वही है जो आप चाहते हैं, लेकिन एक TSTORE के किसी reverted sub-call से बच निकलने पर भरोसा न करें।
  • Slot collisions: assembly में आप raw slots चुनते हैं। एक hashed constant का उपयोग करें, जैसा example में है, ताकि उसी contract में transient storage उपयोग करने वाली किसी दूसरी library से collision न हो।
  • No cross-transaction memory: ऐसे data के लिए transient storage कभी उपयोग न करें जिसे transaction के पार जीवित रहना है। यह एक सस्ता SSTORE नहीं है, यह एक अलग tool है।
  • Chain support: opcodes को Cancun upgrade चाहिए। deploy करने से पहले अपनी target chain और अपने local toolchain (EVM version cancun या उसके बाद पर set) की पुष्टि करें।
  • इसे hands-on अभ्यास करें

    Transient storage को आत्मसात करने का सबसे तेज़ तरीका है guard के दोनों versions लिखना, उन्हें deploy करना, और gas reports को अलग होते देखना। आप यह ठीक-ठीक, एक interactive editor और gas comparisons के साथ, app.solingo-blockchain.xyz पर smart contract exercises में कर सकते हैं। एक persistent guard को transient guard से बदलने की कोशिश करें, फिर एक malicious receiver जोड़ें और पुष्टि करें कि reentrant call अब भी revert होता है। lock को reentrant frame के पार टिके देखना ही वह पल है जब lifetime का नियम स्पष्ट हो जाता है।

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें