web3.py Patterns: Blobs

What are blobs?

The latest Ethereum network upgrade, Dencun, included EIP-4844: Shard Blob Transactions. In short, rollups (L2) can now more cheaply include a batch of transactions on Mainnet Ethereum (L1). A new transaction type was created to handle these "blobs" of data that represent activity on another chain.

If you're looking for more technical detail, there are some great resources to that effect: the EIP linked above, eip4844.com, and ethereum.org's Dencun page should be more than enough to get started with.

How do you use them?

I mean, maybe don't. At least not directly.

As stated, the blob transaction type is intended to support Ethereum's scalability via rollups. There are a finite number of blobs per block – six, to start. When more than six blobs are proposed per block, a fee market forms, and the price to include a blob rises.

Okay, but how do you use them?

Since we all agree that this post is for educational and testnet fun purposes, let's peek at some recent updates to the Python tooling to support the Dencun upgrade.

The broad strokes

As of v0.11.1, eth-account (accessible within web3.py) supports this new transaction type. When signing a transaction, you can now optionally specify some blob data. The pseudocode:

# have an account
acct = w3.eth.account.from_key(private_key)

# generate a transaction
tx = { ... }

# include blob data in a signed transaction
signed_tx = acct.sign_transaction(tx, blobs=[BLOB_DATA])

# broadcast the transaction
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)

Next, let's generate that BLOB_DATA.

The blobification

For example's sake, we'll turn some ASCII art into blob data.

Each blob must contain 4096 32-byte field elements. We're going to encode our ASCII art, then calculate enough empty bytes to fill up the rest of the blob.

# encode some data
text = "<( o.O )>"
encoded_text = abi.encode(["string"], [text])

# subtract the length of the encoded text divided into 32-byte chunks from 4096 and pad the rest with zeros
padding_bytes = (b"\x00" * 32 * (4096 - len(encoded_text) // 32))

BLOB_DATA = padding_bytes + encoded_text

Now that you've got a proper blob payload, you're just about at the finish line. We'll put the rest of the pieces together in the next section.

Side note: if you're already bummed out about the developer experience at this point, well... you're probably not the intended audience. Again, blobs are meant for rollup data, not showcasing ASCII art. 😎

The complete picture

At least at the time of writing, the sample code below should Just Workβ„’ if you supply a provider Sepolia URL and an account with enough Sepolia ether to pay the gas fees.

The only blob-related feature utilized but not yet mentioned is that the new blob transaction type ("type": 3) contains an additional value, "maxFeePerBlobGas". Again, if more than six blobs are proposed for a given block, validators can then choose to prioritize which to include based on that offered max fee. Note that the fee numbers chosen in this example are not especially meaningful.

Last tidbit: the to address of the transaction is zeroed out, but would usually be the settlement contract or a custom "grafitti" address. For our purposes, this value is not important.

import os
from eth_abi import abi
from eth_account import Account
from eth_utils import to_hex
from web3 import Web3, HTTPProvider

w3 = Web3(HTTPProvider('...'))

text = "<( o.O )>"
encoded_text = abi.encode(["string"], [text])

BLOB_DATA = (b"\x00" * 32 * (4096 - len(encoded_text) // 32)) + encoded_text

pkey = os.environ.get("TESTNET_PKEY")
acct = w3.eth.account.from_key(pkey)

tx = {
    "type": 3,
    "chainId": 11155111,  # Sepolia
    "from": acct.address,
    "to": "0x0000000000000000000000000000000000000000",
    "value": 0,
    "maxFeePerGas": 10**12,
    "maxPriorityFeePerGas": 10**12,
    "maxFeePerBlobGas": to_hex(10**12),
    "nonce": w3.eth.get_transaction_count(acct.address),

gas_estimate = w3.eth.estimate_gas(tx)
tx["gas"] = gas_estimate

signed = acct.sign_transaction(tx, blobs=[BLOB_DATA])
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Tx receipt: {tx_receipt}")

If you execute this script, and your provided fees are sufficient, you'll eventually get a transaction receipt hash that you can use to view your blob transaction in a block explorer.

Etherscan's Sepolia block explorer will give you a successful confirmation and a look at the transaction metadata. Here's an example transaction that highlights the blob-related data within the "Click to show more" dropdown or "Blobs" tab.

If you want to view the decoded ASCII art, you can leverage another explorer, Blobscan. In the "Data" section, select UTF-8 under the "View as" dropdown, then scroll all the way down to view the little guy.

Wrapping up

This was probably more fun than useful, but hopefully you learned something new along the way. If there's a temptation to use blob space for something other than the intended purpose, just remember that the raw data is designed to only be accessible within consensus layer clients for about 18 days.

Working on something blob-related? Tell the squad about it or reach out for help in the Ethereum Python Discord Server. See you on the other side. 🐍