Blob Transactions (EIP-4844)

This guide demonstrates how to generate, send, fetch, and verify a Blob Transaction when integrating rollup data into the IoTeX blockchain using EIP-4844 Blob Transactions. These steps ensure scalability and data integrity for rollup-based systems.

Overview

EIP-4844 introduces a new transaction type called Blob Transactions to improve scalability by enabling the temporary storage of large amounts of data outside of a blockchain's execution layer.

Here are key points about the Blob transaction:

  • Blob transactions contain large amounts of data that are not directly stored in the Blockchain's execution layer (i.e., they are not part of the state or permanent history).

  • This data is ephemeral and is retained only for a short period (e.g., 20 days) to facilitate rollup proofs.

  • It is designed to significantly reduce the cost of rollup solutions (Layer 2 scaling technologies) by allowing them to post off-chain data (proofs, commitments) to a Blockchain more efficiently.

  • This off-chain data is essential for rollups to verify transactions but doesn't need to persist in the Blockchain's state.

  • Gas-efficient: Lower costs for rollups to post data compared to traditional calldata.

  • EIP-4844 reduces costs for Layer 2 solutions, setting the stage for Ethereum’s future scalability improvements, such as danksharding.

Blob Data Structure

The blob data structure in EIP-4844 is as follows:

// BlobTxSidecar contains the blobs of a blob transaction.
type BlobTxSidecar struct {
    Blobs       []kzg4844.Blob       // Blob is 128kB bytes array
    Commitments []kzg4844.Commitment // Commitment is 48-bytes
    Proofs      []kzg4844.Proof      // Proof is 48-bytes
}

A Blob represents 128kB of raw data, while a Commitment is a 48-byte cryptographic hash generated by applying a cryptographic commitment scheme (for example, KZG commitment) to the blob data. This provides a compact and efficient method for verifying the integrity of the blob data. The Proof, which is also 48 bytes, is generated by the commitment scheme and allows validators to verify that the blob data corresponds to the on-chain commitment without needing access to the full blob.

How to Generate a Blob Transaction

Blob data contains large amounts of off-chain data, such as rollup proofs from an L2 chain, which are required for rollup verification. In the context of IoTeX, blob data can also include device data generated by IoTeX’s W3bstream devices. This capability enables a wide range of IoT use cases, where data such as sensor readings or event logs can be efficiently bundled and posted to the blockchain.

To create blob data and associated transactions, use the Ethereum SDK or a similar toolkit. See the code example below:

package main

import (
        "github.com/ethereum/go-ethereum/core"
        "github.com/ethereum/go-ethereum/core/types"
        "github.com/ethereum/go-ethereum/crypto/kzg4844"
        "crypto/ecdsa"
        "log"
)

func createBlobTx() *types.BlobTx {
        // Example private key (replace with your own key securely)
        privateKey := "YOUR_PRIVATE_KEY"
        key, err := crypto.HexToECDSA(privateKey)
        if err != nil {
                log.Fatalf("Failed to load private key: %v", err)
        }

        // Prepare blob data
        blobData := []byte("Rollup proof or other off-chain data")
        // or it could be W3bstream device data
        deviceData := []byte("IoT device data from W3bstream")
        
        // generate commitment and proof
        commitment := kzg4844.BlobToCommitment(blobData)
        proof := kzg4844.ComputeBlobProof(blobData, commitment)

        // Create blob data
        blob := BlobTxSidecar {
            Blobs       []kzg4844.Blob{blobData}
            Commitments []kzg4844.Commitment{commitment}
            Proofs      []kzg4844.Proof{proof}
        }
        
        return &types.BlobTx {
            ChainID:   4689, // chain ID of the IoTeX blockchain
            Nonce:     nonce,
            GasTipCap: tipcap,
            GasFeeCap: maxFee,
            Gas:       gas,
            To:        to,
            Data:      calldata,
            BlobFeeCap: blobFeeCap,
            BlobHashes: blob.BlobHashes(),
            Sidecar:    &blob,
        }
}

How to Send the Blob Transaction to IoTeX Blockchain

The IoTeX blockchain is fully compatible with Ethereum, so it can accept Blob Transactions using its RPC interface. See the code example below to send a Blob Transaction to the IoTeX blockchain.

package main

import (
        "log"
        "github.com/ethereum/go-ethereum/core/types"
        "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
        // Connect to IoTeX PRC endpoint
        client, err := ethclient.Dial("https://babel-mainnet.api.iotex.io")
        if err != nil {
                log.Fatalf("Failed to connect to IoTeX: %v", err)
        }
        
        // Example private key (replace with your own key securely)
        privateKey := "YOUR_PRIVATE_KEY"
        key, err := crypto.HexToECDSA(privateKey)
        if err != nil {
                log.Fatalf("Failed to load private key: %v", err)
        }

        // Sign the blob transaction
        tx := types.NewTx(createBlobTx())
        signedTx, err := types.SignTx(tx, types.NewCancunSigner(4689), key)
        if err != nil {
                log.Fatalf("Failed to sign blob transaction: %v", err)
        }

        // Send the transaction
        err := cli.SendTransaction(context.Background(), signedTx)
        if err != nil {
                log.Fatalf("Failed to send transaction: %v", err)
        }
        log.Printf("Transaction Sent: %s", signedTx.Hash().String())
}

Once the Blob Transaction has been successfully sent to the IoTeX blockchain, it will be mined into a block and retained in ephemeral storage for a short period (approximately 20 days).

How to Fetch a Given Blob Transaction from IoTeX Blockchain

You can access the IoTeX RPC endpoint to retrieve any transaction (including a Blob Transaction) by its hash. See the code example below:

package main

import (
        "context"
        "github.com/ethereum/go-ethereum/ethclient"
        "log"
)

func main() {
        // Connect to IoTeX PRC endpoint
        client, err := ethclient.Dial("https://babel-mainnet.api.iotex.io")
        if err != nil {
                log.Fatalf("Failed to connect to IoTeX: %v", err)
        }

        // Query the transaction by hash
        transactionHash := "TRANSACTION_HASH"
        tx, pending, err := cli.TransactionByHash(transactionHash)
        if err != nil {
                log.Fatalf("Failed to fetch transaction: %v", err)
        }

        log.Printf("Fetched Transaction: %v", tx)
}

How to Verify the Received Blob Transaction

Once the Blob Transaction has been retrieved, you can verify it by checking the commitment against the raw blob data. The verification process consists of two steps:

  1. Verify that the lengths of hashes, blobs, commitments, and proofs match, and that the blob hashes match the hashes in the transaction.

  2. Verify the blob data against the commitments and proofs.

See the code example below:

package main

import (
        "github.com/ethereum/go-ethereum/core/types"
        "log"
)

func ValidateBlobTransaction() {
        // tx is received Blob transaction
        sidecar := tx.BlobTxSidecar()
        if sidecar == nil {
            return fmt.Errorf("missing sidecar in blob transaction")
        }
        // Ensure the number of items in the blob transaction and various side
        // data match up before doing any expensive validations
        hashes := tx.BlobHashes()
        if len(hashes) == 0 {
            return fmt.Errorf("blobless blob transaction")
        }
        if len(hashes) > params.MaxBlobGasPerBlock/params.BlobTxBlobGasPerBlob {
            return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), params.MaxBlobGasPerBlock/params.BlobTxBlobGasPerBlob)
        }
        if len(sidecar.Blobs) != len(hashes) {
            return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
        }
        if len(sidecar.Commitments) != len(hashes) {
            return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(sidecar.Commitments), len(hashes))
        }
        if len(sidecar.Proofs) != len(hashes) {
            return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(sidecar.Proofs), len(hashes))
        }
        // Blob quantities match up, validate that the provers match with the
        // transaction hash before getting to the cryptography
        hasher := sha256.New()
        for i, vhash := range hashes {
            computed := kzg4844.CalcBlobHashV1(hasher, &sidecar.Commitments[i])
            if vhash != computed {
                return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, computed, vhash)
            }
        }
        // Blob commitments match with the hashes in the transaction, verify the
        // blobs themselves via KZG
        for i := range sidecar.Blobs {
            if err := kzg4844.VerifyBlobProof(sidecar.Blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil {
                return fmt.Errorf("invalid blob %d: %v", i, err)
            }
        }
        return nil
}

Reference

Last updated

Was this helpful?