Kurt0x

Press enter or click to view image in full size

BlobBytes

This article focuses on how blob transactions work under the hood using ethers.js v6 and kzg-wasm. It is based on practical experimentation, working code examples, and the current limitations around tooling and RPC support.

It covers:

  • common issues when getting started
  • RPC support on Sepolia
  • blob transactions with and without contract calls
  • a walkthrough of a minimal working repository

If you just want to create a blob without diving into code, https://blobsender.xyz provides a simple working example that lets anyone create and submit a blob.

KZG commitments and proofs

Each blob transaction requires:

  • a KZG commitment
  • a KZG proof

These cryptographic objects allow the network to verify that the blob data matches what the transaction claims, without including the blob data directly in execution calldata.

Example:

const blobHex = hexlify(blobBytes);
const commitmentHex = kzg.blobToKZGCommitment(blobHex);
const proofHex = kzg.computeBlobKZGProof(blobHex, commitmentHex);

The resulting values are converted to Uint8Array before being passed to ethers.

Sending a blob transaction

One important detail when sending a blob transaction is the transaction type.

type: 3 is required.
If this field is missing, the transaction will be sent as a normal type 2 transaction and the blob will be ignored.

Example minimal transaction:

await wallet.sendTransaction({
type: 3,
maxFeePerGas,
maxPriorityFeePerGas,
maxFeePerBlobGas,
blobs: [{ data: blobBytes, commitment, proof }],
kzg
});

Blob transaction with a contract call

Blobs can be attached while also executing a contract call. The function call is encoded normally:

const contractInterface = new Interface(CONTRACT_ABI);
const functionData = contractInterface.encodeFunctionData(
'shout',
['ethers-example']
);

The transaction includes both the encoded call data and the blob:

await wallet.sendTransaction({
type: 3,
to: CONTRACT_ADDRESS,
data: functionData,
maxFeePerGas,
maxPriorityFeePerGas,
maxFeePerBlobGas,
blobs: [{ data: blobBytes, commitment, proof }],
kzg
});

This sends a single transaction that:

  • calls a smart contract
  • attaches a blob

Full code example


#!/usr/bin/env tsx
/**
* EIP-4844 Blob Transaction using ethers v6 with Contract Call Example
*/

import { Wallet, JsonRpcProvider, hexlify, toUtf8Bytes, Contract, Interface } from 'ethers';
import { loadKZG } from 'kzg-wasm';
import * as dotenv from 'dotenv';

dotenv.config();

// ============================================================================
// CONTRACT CONFIGURATION
// ============================================================================
// Contract address on Sepolia testnet
const CONTRACT_ADDRESS = '0x7bef193Df28Cd0B164A7227a7904Cdac09f9A1ef';

// Contract ABI - only the function we're calling
const CONTRACT_ABI = [
{
inputs: [{ internalType: 'string', name: 'text', type: 'string' }],
name: 'shout',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
];
// ============================================================================

const RPC_URL = process.env.RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';
const PRIVATE_KEY = process.env.PRIVATE_KEY ?? '';

const provider = new JsonRpcProvider(RPC_URL);
const wallet = new Wallet(PRIVATE_KEY, provider);
const kzg = await loadKZG(); // Load KZG library for blob commitments

console.log('🚀 Sending blob transaction with contract call...\n');

// Prepare blob data - must be exactly 131,072 bytes (padded with zeros if smaller)
const message = 'Blob vibes only 🌈';
const blobBytes = new Uint8Array(131072);
blobBytes.set(toUtf8Bytes(message));

// Compute KZG commitment and proof required for blob transactions
const blobHex = hexlify(blobBytes);
const commitmentHex = kzg.blobToKZGCommitment(blobHex);
const proofHex = kzg.computeBlobKZGProof(blobHex, commitmentHex);

// Convert hex strings to Uint8Array for ethers
const commitmentBytes = Buffer.from(commitmentHex.replace('0x', ''), 'hex');
const commitment = new Uint8Array(commitmentBytes);
const proof = new Uint8Array(Buffer.from(proofHex.replace('0x', ''), 'hex'));

console.log(`Blob data: "${message}"`);

// ============================================================================
// CONTRACT CALL PREPARATION
// ============================================================================
// Create contract instance and encode function call
const contractInterface = new Interface(CONTRACT_ABI);
const functionData = contractInterface.encodeFunctionData('shout', ['ethers-example']);

console.log(`Contract: ${CONTRACT_ADDRESS}`);
console.log(`Function: shout("ethers-example")`);
// ============================================================================

// Send Type 3 (blob) transaction with contract call
const feeData = await provider.getFeeData();
const tx = await wallet.sendTransaction({
type: 3, // Blob transaction type
to: CONTRACT_ADDRESS, // <-- Contract address - blob + contract call
data: functionData, // <-- Encoded function call data
maxFeePerGas: feeData.maxFeePerGas ?? 30n * 10n ** 9n,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 2n * 10n ** 9n,
maxFeePerBlobGas: 400_000_000_000n, // Blob gas fee (400 gwei)
blobs: [{ data: blobBytes, commitment, proof }],
kzg // Required for node verification
});

console.log(`Transaction sent: ${tx.hash}`);
console.log('Waiting for confirmation...\n');

const receipt = await tx.wait();

console.log('✅ Transaction confirmed!');
console.log(` Block: ${receipt?.blockNumber}`);
console.log(` Status: ${receipt?.status === 1 ? 'Success' : 'Failed'}`);
console.log(` Blob Versioned Hashes: ${tx.blobVersionedHashes?.join(', ')}`);
console.log(` Blobscan: https://sepolia.blobscan.com/blob/${tx.blobVersionedHashes?.[0] ?? ''}`);
console.log(` Etherscan: https://sepolia.etherscan.io/tx/${tx.hash}`);