infrablockchain-docs
en
en
  • InfraBlockchain
    • Learn
      • Architecture
        • Architecture
        • Network Participants
        • Parachain
          • System Parachains
      • Protocol
        • System Token
        • Transaction Fee
        • Proof of Transaction
      • Substrate
        • Learn
          • Basic
            • Cryptography
            • Blockchain Basics
            • Consensus
            • Networks and Nodes
            • Blockchain Transaction
            • Transaction Life Cycle
            • Offchain Operations
            • Light Client
            • Rust for Substrate
            • Introduction to Library
            • Architecture and Rust Libraries
            • File Architecture
            • Accounts, Addresses, and Keys
            • Transaction Format
            • Blockchain Randomness
          • FRAME
            • FRAME Pallets
            • FRAME Macros
            • Custom Pallets
            • Pallet Coupling
            • Origin
            • Events and Erros
            • Runtime Storage
            • State Transitions and Storage
            • SCALE Encoding
            • Weight and Fee
            • Runtime API
            • Runtime Development
          • Account
          • Address Format
          • Glossary
          • CLI
            • Archive
            • Memory Profiler
            • Node Template
            • sidecar
            • srtool
            • Subkey
            • subxt
            • try-runtime
            • tx-wrapper
          • Runtime Development
            • Basics
              • Configure Genesis State
              • Configure Runtime Constants
              • Customize a Chain Spec
              • Import a Pallet
              • Use Helper Function
            • Consensus Model
              • PoW
              • Create a Hybrid Node
            • Offchain Worker
              • Request Offchain HTTP
              • Offchain Indexing
              • Offchain Local Storage
            • Pallet Design
              • Create a Storage Structure
              • Implement Lockable Currency
              • Incorporate Randomness
              • Loose Coupling
              • Tight Coupling
            • Parachain Development
              • Add HRMP Channel
              • Add Paranodes
              • Connect to a Local Relay Chain
              • Convert a Solo Chain
              • Prepare to Launch
              • Select Collator
              • Upgrade a Parachain
            • Storage Migration
              • Basics
              • Trigger Migration
            • Test
              • Basics
              • Test a Transfer Transaction
            • Tools
              • Create a TxWrapper
              • Use Sidecar
              • try-runtime
              • Verify WASM
            • Weigths
              • Benchmark
              • Calculate Fees
              • Use Conditional Weights
              • Use Custom Weights
        • Build
          • Decide What to Build
          • Build Process
          • Determinisitc Runtime
          • Chain Spec
          • Genesis Configuration
          • Application Development
          • RPC
          • Troubleshoot Your Code
        • Tutorials
          • Install
            • Developer Tools
            • Linux
            • macOS
            • Rust Toolchain
            • Issues
            • Windows
          • Quick Start
            • Explore the Code
            • Modify Runtime
            • Start a Node
            • Substrate Basics
          • Build a Blockchain
            • Add Trusted Nodes
            • Authorize Specific Nodes
            • Build a Local Blockchain
            • Simulate Network
            • Upgrade a Running Network
          • Build Application Logic
            • Add a Pallet
            • Add Offchasin Workers
            • Publish Custom Pallets
            • Specify Origin for a Call
            • Use Macros in a Custom Pallet
          • Integrate with Tools
            • Access EVM Accounts
            • EVM Integration
            • Explore Sidecar Endpoints
            • Integrate a Light Client Node
          • Smart Contracts
            • Strategy
            • Build a Token Contract
            • Develop a Smart Contract
            • Prepare Your First Contract
            • Troubleshoot Smart Contracts
            • Use Maps for Storing Values
      • XCM
        • XCM
        • XCM Format
    • Service Chains
      • InfraDID
      • InfraEVM
      • URAuth(Universal Resource Auth)
    • DevOps
      • Build
      • Deploy
      • Monitoring
      • Runtime Upgrade
    • Tutorials
      • Basic
        • How to Interact with System Token
        • How To Pay Transaction Fee
        • How To Vote with TaaV
        • Hot to Get Validator Reward
      • Build
        • Build InfraRelayChain
        • Build Parachain
        • Open Message Passing Channels
        • Transfer Assets with XCM
      • Test
        • Benchmark
        • Check Runtime
        • Debug
        • Simulate Parachains
        • Unit Testing
      • Service Chains
        • Play with InfraDID
          • Build
          • Add Keys
          • Add Service Endpoint
          • Create InfraDID
        • Play with InfraEVM
          • Build
          • Deposit and Withdraw Token
          • Deploy ERC20 Contract
          • Deploy ERC721 Contract
          • Deploy ERC1155 Contract
  • Newnal Data Market
Powered by GitBook
On this page
  • Steps
  • Examples
  1. InfraBlockchain
  2. Learn
  3. Substrate
  4. Learn
  5. Runtime Development
  6. Offchain Worker

Offchain Local Storage

PreviousOffchain IndexingNextPallet Design

Last updated 1 year ago

This guide will teach you how to use an offchain worker to save retrieved data in local storage for future access.

In the last section, we mentioned that offchain workers (OCW for short) cannot modify the blockchain state directly, so they have to submit transactions to save their computed results back on-chain. Nonetheless there are also times when the data is not suitable to be saved on-chain yet still needs to be stored somewhere for future access. This includes temporary data or intermediate calculations that can be discarded once the computation is completed.

In this guide, we will instruct offchain workers to write data to the local storage without passing the data among the whole blockchain network. The concept of a Storage Lock is introduced to have value accessible consistently among multiple OCWs. OCWs are asynchronously run at the end of each block production and they are not limited by how long they run, so at any point of time there could be multiple OCW instances being initiated.

The local storage APIs are similar to their on-chain counterpart, using get, set, and mutate to access them. mutate is using a -it compares the content of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail.

Note that as values stored in local storage has not gone through the consensus mechanism among the network, so they are subject to manipulation of the node operator.

In this how-to guide, we will first check from the storage, which acts as a cache, if the computed value exists. If the cached value is found, offchain worker returns; else it will try to acquire a lock, perform the intensive computation, and save it to the storage/cache.

Steps

  1. Define a storage reference in your pallet's offchain_worker function hook:

    fn offchain_worker(block_number: T::BlockNumber) {
      // Create a reference to Local Storage value.
      // Since the local storage is common for all offchain workers, it's a good practice
      // to prepend our entry with the pallet name.
      let storage = StorageValueRef::persistent(b"pallet::my-storage");
    }

    In the above code, a persistent local storage is defined using which is identified by pallet::my-storage key. The key is in a byte array type instead of a str type. This local storage is persisted and shared across runs of the offchain workers.

  2. Check if the storage contains a cached value.

    fn offchain_worker(block_number: T::BlockNumber) {
      // ...
      let storage = StorageValueRef::persistent(b"pallet::my-storage");
    
      if let Ok(Some(res)) = storage.get::<u64>() {
        log::info!("cached result: {:?}", res);
        return Ok(());
      }
    }

    The result is fetched using function, returning a Result<Option<T>, StorageRetrievalError> type. We only care about the case of having a valid value. If yes, return Ok(()). Note we also need to define the type of the returned value.

    Use to write to storage and to read and change the storage atomically.

  3. If there is no valid value (None) or having a StorageRetrievalError, proceed to compute the required result and acquire the storage lock.

    First define the storage lock as follows.

    const LOCK_BLOCK_EXPIRATION: u32 = 3; // in block number
    const LOCK_TIMEOUT_EXPIRATION: u64 = 10000; // in milli-seconds
    
    fn offchain_worker(block_number: T::BlockNumber) {
      // ...
      let storage = StorageValueRef::persistent(b"pallet::my-storage");
    
      if let Ok(Some(res)) = storage.get::<u64>() {
        log::info!("cached result: {:?}", res);
        return Ok(());
      }
    
      // Very intensive computation here
      let val: u64 = 100 + 100;
    
      // Define the storage lock
      let mut lock = StorageLock::<BlockAndTime<Self>>::with_block_and_time_deadline(
        b"pallet::storage-lock",
        LOCK_BLOCK_EXPIRATION,
        Duration::from_millis(LOCK_TIMEOUT_EXPIRATION)
      );
    }

    In the above code snippet, a storage lock is defined with both the . This function takes in a lock identifier, block number expiration, and time expiration. The above lock expires when it either passes the specified amount of block number or time duration. We can also specify the expiration period with or .

  4. Acquire the storage lock using .

    fn offchain_worker(block_number: T::BlockNumber) {
      // ...
    
      let mut lock = /* .... */;
    
      if let Ok(_guard) = lock.try_lock() {
        storage.set(&val);
      }
    }

    It returns Result<StorageLockGuard<'a, '_, L>, <L as Lockable>::Deadline>. The mechanism here is to get a hold of the lock guard first which can only be held by one process at a time before writing to storage. Then the value is written to the storage using set(). The data type of the value passed into set() must be the same as the type specified above in the get<T>() call.

  5. Finally, return from the offchain worker function.

    The full code looks similar to the following:

    const LOCK_BLOCK_EXPIRATION: u32 = 3; // in block number
    const LOCK_TIMEOUT_EXPIRATION: u64 = 10000; // in milli-seconds
    
    fn offchain_worker(block_number: T::BlockNumber) {
      let storage = StorageValueRef::persistent(b"pallet::my-storage");
    
      if let Ok(Some(res)) = storage.get::<u64>() {
        log::info!("cached result: {:?}", res);
        return Ok(());
      }
    
      // Very intensive computation here
      let val: u64 = 100 + 100;
    
      // Define the storage lock
      let mut lock = StorageLock::<BlockAndTime<Self>>::with_block_and_time_deadline(
        b"pallet::storage-lock",
        LOCK_BLOCK_EXPIRATION,
        Duration::from_millis(LOCK_TIMEOUT_EXPIRATION)
      );
    
      if let Ok(_guard) = lock.try_lock() {
        storage.set(&val);
      }
      Ok(())
    }

Examples

compare-and-set pattern
StorageValueRef::persistent()
get<T: Decode>()
set()
mutate<T, E, F>()
block and time deadline specified
just the block number
time duration
.try_lock()
Off-chain worker example pallet in Substrate
OCW pallet demo