Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-8180: Blob Authenticated Messaging

Interfaces for authenticated messaging over EIP-4844 blobs with on-chain decoder discovery

Authors Vitalik Buterin (@vbuterin), Skeletor Spaceman (@skeletor-spaceman)
Created 2026-02-21
Discussion Link https://ethereum-magicians.org/t/erc-8180-blob-authenticated-messaging-bam/27868
Requires EIP-4844, EIP-8179

Abstract

This ERC defines interfaces for decentralized authenticated messaging over EIP-4844 blobs. The core registration interface extends ERC-8179 (Blob Space Segments) with a decoder pointer and a signature registry pointer: the decoder is an untrusted on-chain contract that extracts messages from payloads; the signature registry is a trusted contract that verifies signatures. Separating decoding from verification ensures that a buggy or malicious decoder cannot cause impersonation — it can only produce wrong messages that fail signature verification against the trusted registry.

Three interfaces:

  1. IERC_BAM_Core (extends IERC_BSS): register message batches from blobs or calldata with segment coordinates, decoder address, and signature registry address
  2. IERC_BAM_SignatureRegistry: generic registry for managing public keys across multiple cryptographic schemes (ECDSA, BLS, STARK, etc.) with registration, verification, and aggregation support
  3. IERC_BAM_Exposer: standardized event and query interface for proving individual messages from registered batches on-chain

Supporting definitions:

  • IERC_BAM_Decoder: on-chain contract interface for decoding message payloads (untrusted)
  • Message ID: keccak256(abi.encodePacked(author, nonce, contentHash))
  • Message hash: keccak256(abi.encodePacked(sender, nonce, contents)) — standardized input to the domain-separated signed hash
  • Signing domain: keccak256(abi.encodePacked("ERC-BAM.v1", chainId))

Motivation

EIP-4844 blobs provide 128 KiB of data availability per blob at a fraction of calldata cost. With dictionary-based compression, a single blob holds thousands of messages. Empirical analysis shows capacity exceeding 498 million messages per day. Beyond social messaging, this ERC demonstrates that EIP-4844 blob data is a viable low-cost transport for any signed off-chain message batch.

No standard exists for blob-based messaging. Existing approaches are either minimal and blob-unaware (ERC-3722 Poster, stagnant), NFT-based (ERC-7847, no blob awareness), or L2-specific (Farcaster on Optimism, Lens on zkSync). None standardize the on-chain interfaces for blob-based messaging.

Without a standard, each implementation defines its own batch registration events, key management contracts, message encoding, and exposure mechanisms. Indexers, wallets, and clients cannot interoperate across implementations.

Two design principles guide this ERC:

Anyone can read. A client with an Ethereum node and access to blob data should decode messages from any compliant implementation. The batch registration event contains a decoder address pointing to an on-chain contract that extracts messages from the payload. No dependency on implementation-specific off-chain decoders, proprietary APIs, or centralized indexers.

Capture-minimizing. No privileged decoders, registries, or gatekeepers. Decoder contracts are permissionless to deploy. The decoder address is a per-submission parameter, not a global constant. Implementations choose their own decoders. If a decoder has a bug, deploy a new one; old registrations reference the old decoder, new registrations reference the new one.

This ERC standardizes:

  • Batch registration with segment coordinates, decoder pointer, and signature registry pointer (core extends ERC-8179)
  • Decoder contract interface for message extraction
  • Signature scheme registries for managing public keys across multiple cryptographic schemes (ECDSA, BLS, STARK, etc.)
  • Standardized message hash for trustless client-side verification
  • Message exposure events for on-chain proofs

This ERC does NOT standardize:

  • Message byte layout, batch format, or compression algorithm (decoder-specific)
  • Aggregator protocol, blob data archival (beyond EIP-4844’s ~18-day pruning window), or fee splitting
  • Social features (follows, likes, profiles, threads)
  • The expose() function signature (varies by proof type)

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Architecture Overview

[Blob/calldata] → registerBlobBatch/registerCalldataBatch (Core)
                     ↓ emits BlobBatchRegistered(decoder, registry)
[Indexer sees event] → decoder.decode(payload) → messages + signatureData
                     ↓
[Client computes] → messageHash per message (standardized formula)
                     ↓
[Client verifies] → registry.verify(pubKey, signedHash, sig) → true/false
                     ↓ (optional, for on-chain reactions)
[Anyone calls] → exposer.expose(params) → emits MessageExposed

The protocol has four components. The core contract is the single on-chain entry point: it registers batches and emits events. The decoder is an untrusted contract that extracts messages from payloads. The signature registry is a trusted contract that verifies signatures for a specific cryptographic scheme. The exposer proves individual messages on-chain for smart contract consumption.

Definitions

Term Definition
Decoder An untrusted on-chain contract that extracts messages and signature data from payloads. Implements IERC_BAM_Decoder.
Batch A collection of messages packed into a blob segment or calldata payload. Encoding is decoder-specific.
Message A single message within a batch, containing a sender address, a per-sender nonce, and content bytes.
Message ID keccak256(abi.encodePacked(author, nonce, contentHash)). Unique per message.
Message hash keccak256(abi.encodePacked(sender, nonce, contents)). Standardized hash of a single message. Input to the domain-separated signed hash.
Content hash Identifier for a batch: EIP-4844 versioned hash (blobs) or keccak256(batchData) (calldata).
Signing domain keccak256(abi.encodePacked("ERC-BAM.v1", chainId)). Domain separator for message signatures.
Signature scheme A cryptographic signing algorithm (ECDSA, BLS, STARK, etc.) identified by a 1-byte scheme ID.
Proof of possession A signature proving the registrant controls the private key, preventing rogue key attacks.
Aggregated signature A single signature combining N individual signatures (e.g., BLS aggregation).
Exposure The act of proving a specific message exists within a registered batch and recording that proof on-chain.
Exposer A contract that implements exposure logic for a specific signature scheme and proof type.

Core Registration Interface (IERC_BAM_Core)

The core contract is the protocol’s single on-chain registration point. Aggregators and self-publishing users call registerBlobBatch or registerCalldataBatch to declare that a batch exists. The core stores nothing — it emits events that indexers use to discover batches.

Every compliant core contract MUST implement the IERC_BAM_Core interface, which extends ERC-8179 (IERC_BSS):

interface IERC_BAM_Core is IERC_BSS {
    /// @notice Emitted when a blob batch is registered.
    /// @param versionedHash     The EIP-4844 versioned hash of the blob.
    /// @param submitter         The address that registered the batch (msg.sender).
    /// @param decoder           Decoder contract for extracting messages from the batch payload.
    /// @param signatureRegistry Signature registry for verifying message signatures.
    event BlobBatchRegistered(
        bytes32 indexed versionedHash,
        address indexed submitter,
        address indexed decoder,
        address signatureRegistry
    );

    /// @notice Emitted when a calldata batch is registered.
    /// @param contentHash       Content hash (keccak256 of batch data).
    /// @param submitter         The address that registered the batch (msg.sender).
    /// @param decoder           Decoder contract for extracting messages from the batch payload.
    /// @param signatureRegistry Signature registry for verifying message signatures.
    event CalldataBatchRegistered(
        bytes32 indexed contentHash,
        address indexed submitter,
        address indexed decoder,
        address signatureRegistry
    );

    /// @notice Register a blob batch with segment coordinates, decoder, and signature registry.
    /// @param blobIndex          Index of the blob within the transaction (0-based).
    /// @param startFE            Start field element (inclusive). MUST be < endFE.
    /// @param endFE              End field element (exclusive). MUST be <= 4096.
    /// @param contentTag         Protocol/content identifier (passed to declareBlobSegment).
    /// @param decoder            Decoder contract address for extracting messages.
    /// @param signatureRegistry  Signature registry address for verifying message signatures.
    /// @return versionedHash The EIP-4844 versioned hash of the blob.
    function registerBlobBatch(
        uint256 blobIndex,
        uint16 startFE,
        uint16 endFE,
        bytes32 contentTag,
        address decoder,
        address signatureRegistry
    ) external returns (bytes32 versionedHash);

    /// @notice Register a batch submitted via calldata.
    /// @param batchData          The batch payload bytes.
    /// @param decoder            Decoder contract address for extracting messages.
    /// @param signatureRegistry  Signature registry address for verifying message signatures.
    /// @return contentHash The keccak256 hash of batchData.
    function registerCalldataBatch(
        bytes calldata batchData,
        address decoder,
        address signatureRegistry
    ) external returns (bytes32 contentHash);
}

Behavior

  1. registerBlobBatch MUST call the inherited declareBlobSegment(blobIndex, startFE, endFE, contentTag) from IERC_BSS, which validates segment bounds, retrieves the versioned hash via BLOBHASH, and emits BlobSegmentDeclared. If declareBlobSegment reverts (invalid segment or no blob at index), registerBlobBatch MUST propagate the revert.
  2. Implementations MUST call declareBlobSegment before emitting BlobBatchRegistered, because the versioned hash returned by declareBlobSegment is a required field in the event.
  3. registerBlobBatch MUST emit BlobBatchRegistered with the versioned hash returned by declareBlobSegment, msg.sender, the decoder address, and the signature registry address.
  4. registerBlobBatch MUST return the versioned hash.
  5. registerCalldataBatch MUST compute contentHash as keccak256(batchData).
  6. registerCalldataBatch MUST emit CalldataBatchRegistered with the content hash, msg.sender, the decoder address, and the signature registry address.
  7. registerCalldataBatch MUST return the content hash.
  8. Core implementations MUST NOT write to storage. The event log is the sole record.
  9. Both functions MUST be permissionless: any address MAY call them.
  10. A decoder address of address(0) is permitted but NOT RECOMMENDED. It indicates no on-chain decoder is available for the batch, weakening the “anyone can read” property.
  11. A signatureRegistry address of address(0) is permitted. It indicates the batch is unsigned or uses an off-chain verification mechanism. Clients receiving signatureRegistry=address(0) SHOULD treat messages as unverified. Use cases for unsigned batches include public announcements, advertisements, or data feeds where per-message authorship verification is not required. The submitter field in the event provides batch-level accountability.

Relationship to ERC-8179

Since IERC_BAM_Core extends IERC_BSS, every BAM contract is also a BSS contract. registerBlobBatch emits both BlobSegmentDeclared (from the inherited declareBlobSegment call) and BlobBatchRegistered. BSS indexers tracking BlobSegmentDeclared events discover the segment boundaries. BAM indexers tracking BlobBatchRegistered events discover the batch, its decoder, and its signature registry.

BAM contracts do not require a shared singleton deployment. Each BAM deployment functions as its own BSS instance. Indexers filter by event topic hash (globally indexed on Ethereum), not by contract address.

For protocols that need BSS without BAM (e.g., non-messaging data in shared blobs), a standalone BSS contract remains the correct choice. BAM is for message batches that benefit from decoder and signature registry discovery.

For calldata batches (registerCalldataBatch), no segment declaration is needed. Calldata has no blob, no versioned hash, and no field element range. The IERC_BSS extension applies only to the blob path.

Shared blob segment allocation requires off-chain coordination between parties before the blob transaction is constructed.

Decoder Interface (IERC_BAM_Decoder)

The decoder extracts individual messages and signature data from a raw batch payload. Given raw bytes from a blob segment or calldata batch, a decoder returns the decoded messages and opaque signature data. Decoders are untrusted: a lying decoder produces wrong messages whose hashes fail verification against the trusted registry. Because decoders cannot affect verification outcomes, anyone can deploy one for any encoding format.

interface IERC_BAM_Decoder {
    /// @notice A decoded message.
    struct Message {
        address sender;
        uint64 nonce;
        bytes contents;
    }

    /// @notice Decodes all messages and extracts signature data from the payload.
    /// @param payload Raw message batch bytes.
    /// @return messages      Array of decoded messages (sender + nonce + contents).
    /// @return signatureData Opaque signature bytes (e.g., aggregated BLS signature,
    ///                       concatenated ECDSA signatures). Format depends on the
    ///                       signature scheme; length is derivable from
    ///                       signatureRegistry.signatureSize() and message count.
    function decode(bytes calldata payload)
        external view returns (Message[] memory messages, bytes memory signatureData);
}

Behavior

  1. decode MUST return all messages in the payload as an array of Message structs. Each message contains the sender’s Ethereum address, a per-sender monotonically increasing nonce, and the content bytes.
  2. decode MUST return an empty array and empty bytes for an empty payload.
  3. decode MUST return the raw signature data as opaque bytes. The format is scheme-specific: for BLS, this is the aggregated signature (96 bytes); for ECDSA, this is the concatenated individual signatures (65 bytes each).
  4. Decoder behavior MUST be deterministic: given the same payload, repeated calls MUST return the same result.
  5. Decoder contracts MAY read external state (e.g., shared compression dictionaries) but MUST NOT produce side effects.
  6. Decoders MUST NOT perform signature verification. Verification is the client’s responsibility using the trusted registry.

Decoder design guidance

v1 decoders SHOULD use simple encodings: ABI-encoded arrays, RLP, or SSZ. At minimum, a v1 decoder extracts a list of (sender: address, nonce: uint64, contents: bytes) tuples and appended signature data. Complex compression (dictionary-based, delta encoding) can be introduced in later decoder versions. The IERC_BAM_Decoder interface is encoding-agnostic; a v2 decoder with on-chain decompression implements the same function.

Nonce Semantics

Nonces MUST be per-sender monotonically increasing within the signing protocol. A decoder MAY return messages with non-sequential nonces. Clients SHOULD treat messages whose nonce does not exceed the last-accepted nonce for that sender as invalid.

If a decoder returns duplicate (sender, nonce) pairs, the resulting message IDs collide. Clients MUST de-duplicate by messageId.

Nonce correctness is enforced by the signing protocol: the signer includes the correct nonce in the signed hash. An incorrect nonce produces a different messageHash, causing signature verification to fail.

Signature Registry Interface (IERC_BAM_SignatureRegistry)

The signature registry maps Ethereum addresses to public keys and provides signature verification for a specific cryptographic scheme (ECDSA, BLS, STARK, etc.). One registry per scheme is expected — roughly four for the foreseeable future.

Every compliant signature registry MUST implement the IERC_BAM_SignatureRegistry interface:

interface IERC_BAM_SignatureRegistry {
    event KeyRegistered(address indexed owner, bytes pubKey, uint256 index);

    error AlreadyRegistered(address owner);
    error NotRegistered(address owner);
    error InvalidProofOfPossession();
    error InvalidPublicKey();
    error InvalidSignature();
    error VerificationFailed();

    // Scheme identification
    function schemeId() external pure returns (uint8 id);
    function schemeName() external pure returns (string memory name);
    function pubKeySize() external pure returns (uint256 size);
    function signatureSize() external pure returns (uint256 size);

    // Registration
    function register(bytes calldata pubKey, bytes calldata popProof)
        external returns (uint256 index);
    function getKey(address owner)
        external view returns (bytes memory pubKey);
    function isRegistered(address owner)
        external view returns (bool registered);

    // Verification
    function verify(
        bytes calldata pubKey,
        bytes32 messageHash,
        bytes calldata signature
    ) external view returns (bool valid);
    function verifyWithRegisteredKey(
        address owner,
        bytes32 messageHash,
        bytes calldata signature
    ) external view returns (bool valid);

    // Aggregation
    function supportsAggregation() external pure returns (bool supported);
    function verifyAggregated(
        bytes[] calldata pubKeys,
        bytes32[] calldata messageHashes,
        bytes calldata aggregatedSignature
    ) external view returns (bool valid);
}

Behavior

  1. schemeId() MUST return a unique 1-byte identifier for the signature scheme. Assigned IDs:

    ID Scheme
    0x01 ECDSA-secp256k1
    0x02 BLS12-381
    0x03 STARK-Poseidon
    0x04 Dilithium
    0x05-0xFF Reserved
  2. register MUST validate the proof of possession before registering the key. The proof format is scheme-specific. For BLS12-381, this is a signature over a domain-separated message binding the BLS key to the caller’s Ethereum address.
  3. register MUST revert with AlreadyRegistered if the address already has a registered key.
  4. register MUST revert with InvalidProofOfPossession if the proof is invalid.
  5. register MUST revert with InvalidPublicKey if the key format is invalid.
  6. register MUST emit KeyRegistered with the owner, public key, and assigned index.
  7. verify MUST return true if the signature is valid for the given public key and message hash, false otherwise. The messageHash parameter is the final hash that was signed (after domain separation). Domain separation is the caller’s responsibility; the registry is domain-unaware. verify MUST NOT revert on invalid signatures. It MAY revert with InvalidSignature if the signature bytes are malformed (e.g., wrong length for the scheme).
  8. verifyWithRegisteredKey MUST revert with NotRegistered if the owner has no registered key. Otherwise, it MUST behave identically to verify using the owner’s registered key.
  9. supportsAggregation MUST return true if the scheme supports signature aggregation (e.g., BLS), false otherwise (e.g., ECDSA).
  10. verifyAggregated MUST revert if supportsAggregation() returns false.
  11. The VerificationFailed error is available for implementation-specific methods (e.g., expose functions) that require verification to succeed rather than returning a boolean.
  12. Scheme-specific extensions (key rotation, revocation, index lookups) MAY be added by extending this interface. They are out of scope for this ERC. Key rotation semantics vary by scheme (BLS rotation requires a new proof of possession; ECDSA rotation may use ecrecover). The base interface deliberately excludes rotation to avoid prescribing scheme-specific behavior.
  13. The base interface does not prevent two addresses from registering the same public key. Both would pass proof-of-possession (proving they hold the private key). Registries MAY enforce key uniqueness; if they do not, signature verification is ambiguous for shared keys. This is the registrant’s responsibility.

For BLS-based registries, every sender whose messages appear in a batch MUST have a registered public key before those messages can be verified or exposed. ECDSA-based registries MAY allow keyless verification via ecrecover-style key derivation.

Message Exposure Interface (IERC_BAM_Exposer)

The exposer proves on-chain that a specific message exists in a registered batch and that its signature is valid. This enables on-chain contracts to react to specific messages (governance, token gates, dispute resolution).

Every compliant exposer contract MUST implement the IERC_BAM_Exposer interface:

interface IERC_BAM_Exposer {
    event MessageExposed(
        bytes32 indexed contentHash,
        bytes32 indexed messageId,
        address indexed author,
        address exposer,
        uint64 timestamp
    );

    error NotRegistered(bytes32 contentHash);
    error AlreadyExposed(bytes32 messageId);

    function isExposed(bytes32 messageId) external view returns (bool exposed);
}

Behavior

  1. When an implementation’s expose function successfully verifies and records a message, it MUST emit MessageExposed with the content hash, message ID, author, msg.sender, and uint64(block.timestamp).
  2. The messageId MUST be computed as keccak256(abi.encodePacked(author, nonce, contentHash)).
  3. Implementations MUST track exposed message IDs and revert with AlreadyExposed if a message is exposed twice.
  4. isExposed MUST return true if a MessageExposed event has been emitted for the given messageId by this contract, false otherwise.
  5. Implementations SHOULD verify that the content hash corresponds to a registered batch (via the core contract) and revert with NotRegistered if not.
  6. The expose function signature itself is NOT standardized. It varies by signature scheme (ECDSA vs BLS), proof type (KZG point evaluation, ZK proof, merkle proof), and data source (blob vs calldata). Implementations define their own expose methods and emit the standardized event.

Why expose() is not standardized

Different signature schemes and proof types require fundamentally different parameters:

  • BLS + KZG: Requires BLS signature, KZG commitment, point evaluation proof, field element indices
  • ECDSA + Merkle: Requires ECDSA signature, merkle proof, leaf index
  • STARK + ZK: Requires STARK proof, public inputs
  • Calldata: Requires message bytes, offset, signature (no KZG proof needed)

Forcing these into one function signature would either be too generic (a single bytes parameter losing type safety) or too restrictive (excluding future proof types). The event is the interoperability surface: any exposer, regardless of proof mechanism, emits MessageExposed.

Message ID Convention

Message IDs MUST be computed as:

messageId = keccak256(abi.encodePacked(author, nonce, contentHash))

Where:

  • author is the message author’s Ethereum address (address, 20 bytes)
  • nonce is a per-author monotonically increasing counter (uint64, 8 bytes)
  • contentHash is the batch identifier (bytes32, 32 bytes): versioned hash for blob batches, keccak256(batchData) for calldata batches

The result is a globally unique, deterministic identifier per message. The nonce prevents collisions when an author publishes multiple messages in the same batch.

Signing Domain and Message Hash Convention

Message hashes MUST be computed as:

messageHash = keccak256(abi.encodePacked(sender, nonce, contents))

Where sender is the message author’s Ethereum address (address, 20 bytes), nonce is the per-sender monotonically increasing counter (uint64, 8 bytes), and contents is the message content (bytes, variable length).

Message signatures MUST use a domain separator to prevent cross-chain replay:

domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))

Where chainId is the EIP-155 chain ID (uint256). The signed message hash is then:

signedHash = keccak256(abi.encodePacked(domain, messageHash))

The standardized messageHash formula enables trustless verification: a client computes hashes from the decoder’s output and verifies them against the trusted registry. If the decoder lies about message contents, the client computes wrong hashes that fail signature verification. The decoder can cause false negatives (valid messages rejected) but never false positives (forged messages accepted).

Worked Examples

Example 1: Aggregator Submits a Blob Batch

An aggregator collects 500 messages, encodes them using a v1 decoder format, packs the payload into a blob, and submits:

Transaction:
  1. Submit blob (type-3 tx with 1 blob)
  2. core.registerBlobBatch(
         0, 0, 4096, keccak256("social-blobs.v4"), decoderAddr, sigRegistryAddr
     )
     → declareBlobSegment(0, 0, 4096, keccak256("social-blobs.v4"))
       → emits BlobSegmentDeclared(vHash, aggregator, 0, 4096, contentTag)
     → emits BlobBatchRegistered(vHash, aggregator, decoderAddr, sigRegistryAddr)

Client verification (by anyone):
  1. DECODE (untrusted)
     - See BlobBatchRegistered → get versionedHash, decoderAddr, sigRegistryAddr
     - Fetch blob data via versioned hash
     - (messages, signatureData) = decoder.decode(blobData)
       → 500 messages with sender, nonce, contents

  2. COMPUTE HASHES (client, standardized)
     - domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
     - for each message:
         messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
         signedHash  = keccak256(abi.encodePacked(domain, messageHash))

  3. VERIFY (trusted registry)
     - pubKeys = [registry.getKey(m.sender) for m in messages]
     - registry.verifyAggregated(pubKeys, signedHashes, signatureData) → true

Gas: ~21,000 (intrinsic) + ~3,500 (declareBlobSegment) + ~2,400 (BlobBatchRegistered event) + blob gas. Total BAM overhead: ~5,900 gas.

Example 2: User Self-Publishes via Calldata

A user bypasses aggregators and publishes a single-message batch:

Transaction:
  1. core.registerCalldataBatch(batchData, decoderAddr, sigRegistryAddr)
     → computes contentHash = keccak256(batchData)
     → emits CalldataBatchRegistered(
           contentHash, user, decoderAddr, sigRegistryAddr
       )

Client verification:
  1. DECODE
     - See CalldataBatchRegistered → get calldata from tx, decoderAddr, sigRegistryAddr
     - (messages, signatureData) = decoder.decode(batchData)
       → 1 message

  2. COMPUTE HASHES
     - domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
     - messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
     - signedHash  = keccak256(abi.encodePacked(domain, messageHash))

  3. VERIFY
     - pubKey = registry.getKey(message.sender)
     - registry.verify(pubKey, signedHash, signatureData) → true

Example 3: Shared Blob with L2 Rollup

An aggregator shares a blob with a rollup. The rollup uses field elements 0-1999; the messaging protocol uses 2000-4095. Shared blob segment allocation requires off-chain coordination between parties before the blob transaction is constructed.

Transaction:
  1. rollup.submitBatch(...)                                           // L2 data
  2. bss.declareBlobSegment(0, 0, 2000, keccak256("optimism.bedrock")) // standalone BSS
  3. core.registerBlobBatch(                                           // BAM (extends BSS)
         0, 2000, 4096, keccak256("social-blobs.v4"), decoderAddr, sigRegistryAddr
     )
     → emits BlobSegmentDeclared(vHash, aggregator, 2000, 4096, ...)
     → emits BlobBatchRegistered(vHash, aggregator, decoderAddr, sigRegistryAddr)

Events:
  - BlobSegmentDeclared [0, 2000) "optimism.bedrock"      (standalone BSS)
  - BlobSegmentDeclared [2000, 4096) "social-blobs.v4"    (BAM contract)
  - BlobBatchRegistered (decoderAddr, sigRegistryAddr)     (BAM contract)

Client verification:
  1. See BlobBatchRegistered → know FE range from BlobSegmentDeclared on same contract
  2. Fetch blob, read FE [2000, 4096)
  3. (messages, signatureData) = decoder.decode(segmentData)
  4. Compute messageHash and signedHash for each message (standardized)
  5. registry.verifyAggregated(pubKeys, signedHashes, signatureData) → true

Example 4: Key Registration and Message Exposure

A user registers a BLS key; later a message is exposed on-chain:

Setup:
  1. blsRegistry.register(blsPubKey, popSignature)
     → emits KeyRegistered(user, blsPubKey, index)

Exposure (by anyone, permissionless):
  2. exposer.expose(params)  // implementation-specific function
     → decodes message via decoder
     → computes messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
     → computes signedHash  = keccak256(abi.encodePacked(domain, messageHash))
     → verifies BLS signature against registered key via registry
     → verifies KZG proof against versioned hash
     → emits MessageExposed(contentHash, messageId, author, exposer, timestamp)

Query:
  3. exposer.isExposed(messageId) → true

Rationale

BAM extends BSS

The original design defined registerBlobBatch(blobIndex) with no segment coordinates. When sharing blobs with other protocols, callers needed a separate ERC-8179 call to declare their segment. Two calls to two contracts created a correlation problem: a shared blob with N segments produces N BlobSegmentDeclared events and one BlobBatchRegistered event, all referencing the same versioned hash. No on-chain link connected the BAM batch to its specific BSS segment.

Inheriting IERC_BSS eliminates the ambiguity. registerBlobBatch declares the segment and registers the batch atomically: one call, two events, unambiguous correlation.

BAM contracts do not require a singleton deployment. Each deployment emits BlobSegmentDeclared from its own address. Ethereum event topics are globally indexed; indexers filter by topic hash, not by contract address. The singleton pattern in BSS was a simplicity choice, not a requirement.

Trust separation: decoder vs registry

The original design bundled decoding and verification in a single “schema” contract. If a client trusts a bad schema, anyone can impersonate any address (the schema’s verify could always return true). Separating decoding (untrusted, permissionless) from verification (trusted, few instances) eliminates this risk.

Decoders are “open permissionless innovation” — many exist, anyone can deploy one, and a buggy decoder causes only false negatives (valid messages rejected), never false positives (forged messages accepted). Registries are “mostly ~4 highly-audited instances” — one per signature scheme. The trust surface is narrow and auditable.

The standardized messageHash = keccak256(abi.encodePacked(sender, nonce, contents)) formula is the bridge: the client computes hashes from the decoder’s (untrusted) output, then verifies them against the registry’s (trusted) verification. If the decoder lies, the hashes are wrong, and verification fails.

Component Trust Count Risk of lying
Decoder Untrusted Many Low — wrong output fails signature verification
Registry Trusted ~4 High — wrong verification enables impersonation

On-chain decoder discovery

A decoder contract is a Solidity contract deployed on-chain that extracts messages and signature data from payloads. Given raw bytes, it returns an array of Message structs (sender + nonce + contents) and opaque signature bytes. The decoder address is emitted in the registration event, discoverable from the event log alone.

Traditional approaches embed decoding logic in off-chain clients. If a protocol changes its encoding, every client needs an update. On-chain decoders invert this: the decoder is on-chain, auditable, and callable by any contract or client.

v1 decoders use simple encodings (ABI, RLP, or SSZ). Complex compression (zstd with shared dictionaries, delta encoding) can be introduced in later decoder versions. The IERC_BAM_Decoder interface is encoding-agnostic; decoder upgrades do not change the interface.

“Anyone can read”

A client that (a) has access to an Execution Layer node (for event logs and transaction data) and (b) has access to a Consensus Layer node or blob archival service (for raw blob data) decodes and verifies messages from any BAM-compliant implementation:

  1. Scan BlobBatchRegistered events for the decoder address, signature registry address, and versioned hash.
  2. Fetch blob data via the versioned hash.
  3. Call decoder.decode(payload) to extract messages and signature data.
  4. Compute messageHash and signedHash for each message (standardized formula).
  5. Call registry.verifyAggregated(pubKeys, signedHashes, signatureData) to validate.

No dependency on implementation-specific indexers, aggregators, or off-chain APIs. The on-chain decoder is the canonical extractor. Proprietary encodings without on-chain decoders are permitted (decoder = address(0)) but create centralization pressure: users depend on the protocol’s off-chain decoder, which is a capture vector.

Capture-minimizing design

Decoder contracts are permissionless to deploy. The decoder address is a parameter in registerBlobBatch, not a value read from a registry. No governance, no approval, no gatekeeping. Different implementations use different decoders. Different versions of the same implementation use different decoders. Nothing prevents forking a decoder contract and deploying a modified version.

Zero-storage core

The core contract emits events and stores nothing. The same rationale applies to ERC-3722 (Poster) and ERC-8179: the event log suffices for indexing, and avoiding SSTORE keeps registration costs minimal.

registerBlobBatch costs approximately 5,900 gas (segment validation + two event emissions). Gas estimates are approximate, based on Cancun EVM pricing, and verified against the reference implementation’s forge benchmarks. Message registration executes alongside blob transactions costing 21,000+ gas base plus blob gas. Under 6,000 gas overhead adds under 29% to the cheapest possible blob transaction.

Exposure as a separate contract

The core contract registers data without interpreting it. Exposure (proving a specific message exists in a batch) requires signature verification, proof validation, and scheme-specific logic. Combining registration and exposure in one contract couples proof-type support to batch registration, forcing all implementations to support the same verification mechanisms.

Separating core and exposer allows:

  • One core contract serving multiple exposers (BLS exposer, ECDSA exposer, ZK exposer)
  • Exposer upgrades without touching the core
  • Different trust models (core is trustless; exposers may have scheme-specific assumptions)

Why the expose function is not standardized

BLS+KZG requires different parameters than ECDSA+Merkle or STARK+ZK. Forcing one function signature would either lose type safety or exclude future proof types. The event provides the interoperability surface: any exposer, regardless of proof mechanism, emits MessageExposed. Smart contracts and indexers react to the event, not the function.

Generic signature registry

A single IERC_BAM_SignatureRegistry interface works across ECDSA, BLS, STARK, and future schemes. This avoids N separate standards for N schemes. The schemeId byte and supportsAggregation flag are the only scheme-specific metadata; everything else (register, verify, getKey) is uniform.

BLS12-381 is the primary use case today (signature aggregation saves 79-94% of authentication overhead depending on batch size), but the interface supports post-quantum schemes (Dilithium) and ZK-friendly schemes (STARK-Poseidon) without modification.

The signature registry interface is reusable by any protocol needing on-chain key management and multi-scheme signature verification. Future ERCs may adopt or extend this interface as a standalone registry standard.

Message ID determinism

keccak256(abi.encodePacked(author, nonce, contentHash)) is deterministic and computable from the message data alone, requiring no on-chain state. The author address prevents cross-user collisions, the nonce prevents same-batch collisions, and the content hash binds the ID to a specific batch.

Domain separator for signing

The "ERC-BAM.v1" prefix prevents signature reuse across protocols; chainId prevents cross-chain replay. For individual user self-publication with ECDSA, adopters may define an EIP-712 TypedData struct matching the messageHash fields for improved wallet display. The core standard does not mandate EIP-712 because aggregated BLS signing (the primary blob path) uses headless signing where wallet display provides no benefit.

Backwards Compatibility

This ERC introduces new interfaces and does not modify any existing standards.

Existing messaging contracts (e.g., ERC-3722 Poster) can adopt this ERC by:

  1. Implementing IERC_BAM_Core directly (includes IERC_BSS by inheritance)
  2. Deploying a standalone core contract and calling it within the same transaction

The BLOBHASH opcode (EIP-4844) is required for registerBlobBatch. The calldata path (registerCalldataBatch) works on any EVM chain.

Test Cases

Core Registration

Function Input Expected Result
registerBlobBatch(0, 0, 4096, tag, decoder, sigReg) Blob at index 0, full blob Emits BlobSegmentDeclared + BlobBatchRegistered, returns versioned hash
registerBlobBatch(99, 0, 4096, tag, decoder, sigReg) No blob at index 99 Reverts NoBlobAtIndex(99)
registerBlobBatch(0, 4096, 0, tag, decoder, sigReg) Invalid segment Reverts InvalidSegment(4096, 0)
registerBlobBatch(0, 0, 5000, tag, decoder, sigReg) endFE out of range Reverts InvalidSegment(0, 5000)
registerCalldataBatch(data, decoder, sigReg) 1,000 bytes of batch data Emits CalldataBatchRegistered with keccak256 hash
registerCalldataBatch(data, address(0), sigReg) No decoder Emits CalldataBatchRegistered with decoder=address(0)

Decoder

Function Input Expected Result
decode(payload) 500-message batch Returns 500 Message structs + aggregated signature bytes
decode(empty) Empty payload Returns empty array + empty bytes
decode(payload) Valid BLS batch Returns messages and 96-byte aggregated BLS signature
decode(payload) Valid ECDSA batch Returns messages and N*65-byte concatenated ECDSA signatures

Signature Registry

Function Input Expected Result
schemeId BLS registry Returns 0x02
schemeName BLS registry Returns "BLS12-381"
pubKeySize BLS registry Returns 48
signatureSize BLS registry Returns 96
register Valid BLS key + PoP Emits KeyRegistered, returns index
register Already registered address Reverts AlreadyRegistered
register Invalid PoP signature Reverts InvalidProofOfPossession
register Malformed public key Reverts InvalidPublicKey
getKey Registered address Returns the registered public key
getKey Unregistered address Returns empty bytes
isRegistered Registered address Returns true
isRegistered Unregistered address Returns false
verify Valid signature Returns true
verify Invalid signature Returns false
verifyWithRegisteredKey Registered owner, valid sig Returns true
verifyWithRegisteredKey Unregistered owner Reverts NotRegistered
supportsAggregation BLS registry Returns true
supportsAggregation ECDSA registry Returns false
verifyAggregated Valid aggregated BLS sig Returns true
verifyAggregated ECDSA registry (no agg) Reverts

Message Exposure

Function Input Expected Result
isExposed Unexposed message ID Returns false
isExposed Exposed message ID Returns true
Expose call Valid proof + signature Emits MessageExposed, returns message ID
Expose call Unregistered batch Reverts NotRegistered
Expose call Already exposed message Reverts AlreadyExposed

Message ID

Author (address) Nonce Content Hash Expected Message ID
0xABCD...0001 0 0x1234...5678 keccak256(abi.encodePacked(author, 0, hash))
0xABCD...0001 1 0x1234...5678 Different from nonce=0 (same batch, different ID)

Reference Implementation

A reference implementation exists for the signature registry and exposer interfaces. The existing contracts predate the decoder/signature-registry separation and BSS-extension features of this ERC and use protocol-specific naming; they are functionally equivalent to the standardized interfaces for signature registry and exposure:

  • Signature Registry: BLSRegistry.sol (implements ISignatureRegistry, equivalent to IERC_BAM_SignatureRegistry, for BLS12-381 with key rotation and revocation extensions)
  • Exposer: BLSExposer.sol (functionally equivalent to IERC_BAM_Exposer with KZG point evaluation proofs and BLS signature verification)

Updating the reference contracts to implement the ERC interfaces directly (with ERC naming) is tracked as a separate task.

Deployed on Sepolia:

Contract Address
SocialBlobsCore 0xAdd498490f0Ffc1ba15af01D6Bf6374518fE0969
BLSRegistry 0x2146758C8f24e9A0aFf98dF3Da54eef9f53BCFbf
BLSExposer 0x0136454b435fE6cCa5F7b8A6a8cFB5B549afB717

Minimal Core Implementation

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;

import {IERC_BAM_Core} from "./IERC_BAM_Core.sol";

contract BlobAuthenticatedMessagingCore is IERC_BAM_Core {
    uint16 internal constant MAX_FIELD_ELEMENTS = 4096;

    /// @inheritdoc IERC_BSS
    function declareBlobSegment(
        uint256 blobIndex,
        uint16 startFE,
        uint16 endFE,
        bytes32 contentTag
    ) public returns (bytes32 versionedHash) {
        if (startFE >= endFE || endFE > MAX_FIELD_ELEMENTS) {
            revert InvalidSegment(startFE, endFE);
        }

        assembly {
            versionedHash := blobhash(blobIndex)
        }
        if (versionedHash == bytes32(0)) revert NoBlobAtIndex(blobIndex);

        emit BlobSegmentDeclared(versionedHash, msg.sender, startFE, endFE, contentTag);
    }

    /// @inheritdoc IERC_BAM_Core
    function registerBlobBatch(
        uint256 blobIndex,
        uint16 startFE,
        uint16 endFE,
        bytes32 contentTag,
        address decoder,
        address signatureRegistry
    ) external returns (bytes32 versionedHash) {
        versionedHash = declareBlobSegment(blobIndex, startFE, endFE, contentTag);

        emit BlobBatchRegistered(
            versionedHash, msg.sender, decoder, signatureRegistry
        );
    }

    /// @inheritdoc IERC_BAM_Core
    function registerCalldataBatch(
        bytes calldata batchData,
        address decoder,
        address signatureRegistry
    ) external returns (bytes32 contentHash) {
        contentHash = keccak256(batchData);

        emit CalldataBatchRegistered(
            contentHash, msg.sender, decoder, signatureRegistry
        );
    }
}

Security Considerations

Segment overlap

Segment overlap — two declarations claiming overlapping field element ranges in the same blob — is not prevented on-chain. Clients must detect overlap by cross-referencing BlobSegmentDeclared events sharing the same versioned hash.

Batch registration spam

Registering a blob batch requires a type-3 transaction with at least one blob (~21,000 intrinsic gas plus blob gas fees). Registering a calldata batch costs calldata gas proportional to data size. Both are self-limiting: spam costs the spammer gas without affecting other users. The core contract stores nothing, so spam events increase log volume but not state bloat.

Decoder trust model

A decoder contract is user-deployed code. It may contain bugs, return incorrect Message structs, or consume excessive gas. However, because decoders do not verify signatures, a buggy decoder cannot cause impersonation. If a decoder returns wrong messages, the client computes wrong hashes that fail verification against the trusted registry. The worst case is denial of service (valid messages rejected), not forgery (fake messages accepted).

A decoder behind an upgradeable proxy could change behavior after deployment. This is lower-risk than in the bundled schema design because the decoder cannot affect verification outcomes, but consumers should still verify whether a decoder is immutable for defense in depth.

Registry trust model

Signature registries are the trusted component. A malicious or buggy registry could return incorrect verification results, enabling impersonation. The number of registries is intentionally small (~one per signature scheme) to minimize the audit surface. Consumers should verify that the signatureRegistry address in a BlobBatchRegistered event corresponds to a known, audited implementation before trusting verification results.

A registry behind an upgradeable proxy is a critical risk: it could be changed to accept any signature. Registries should be deployed as immutable contracts.

Decoder denial of service

A malicious decoder could execute unbounded computation in decode, consuming excessive gas. On-chain callers (e.g., exposer contracts) should set gas limits when calling decoder functions. Off-chain callers (indexers, clients) should enforce execution timeouts.

Key squatting in signature registries

A malicious actor could register a key for an address before the legitimate owner. The proof of possession requirement prevents this: register requires a signature proving the caller controls the private key corresponding to the public key being registered. An attacker cannot register someone else’s key without their private key.

Rogue key attacks (aggregation)

BLS signature aggregation is vulnerable to rogue key attacks where a malicious signer crafts a public key that cancels out honest signers’ contributions. The mandatory proof of possession in register mitigates this by ensuring every registered key has a corresponding private key holder.

Cross-chain replay

The signing domain convention includes chainId, preventing signatures from being replayed on other chains. Implementations should use the domain separator when computing signed message hashes.

Message hash and message ID collisions

The message hash is keccak256(abi.encodePacked(sender, nonce, contents)). The abi.encodePacked encoding is unambiguous because sender (20 bytes) and nonce (8 bytes) are fixed-size, so the variable-length contents field always begins at byte 28. No two distinct (sender, nonce, contents) tuples produce the same packed encoding.

The message ID is keccak256(abi.encodePacked(author, nonce, contentHash)). All three fields are fixed-size (20 + 8 + 32 bytes), so the encoding is trivially unambiguous.

For an attacker to find two distinct inputs that produce the same hash for either formula requires a collision attack on keccak256 (birthday bound ~2^128 security). Finding a second input that matches a specific existing hash requires a preimage or second preimage attack (~2^256 security). Both are computationally infeasible.

Exposure replay

The AlreadyExposed error and isExposed query prevent the same message from being exposed twice. Implementations must maintain a mapping of exposed message IDs. This is the one required storage operation in the exposure interface.

Content hash binding

BlobBatchRegistered binds a versioned hash to a submitter. The versioned hash is retrieved via BLOBHASH, which only returns non-zero values for blobs in the current transaction. An attacker cannot register a batch for someone else’s blob; they would need to include the blob in their own transaction.

For calldata batches, the content hash is keccak256(batchData), which is deterministic. Anyone can register the same calldata, but the submitter field distinguishes registrations.

Blob data pruning

EIP-4844 blob data is pruned after ~18 days. Batch registration events persist indefinitely, but the underlying blob data may become unavailable. Implementations should consider archival strategies for blob data preservation. Message exposure creates a permanent on-chain record of individual messages, which survives blob pruning.

Unverified batch content

The core contract registers batches without inspecting their content. A registered batch may contain malformed, empty, or malicious data. Registration is a claim that a batch exists, not a guarantee of its validity. Indexers and exposers must independently validate batch content.

Exposer trust model

Different exposers have different trust assumptions. A KZG-based exposer provides cryptographic proof that a message was in a blob. A merkle-based exposer provides proof against a merkle root. The MessageExposed event does not indicate the proof type; consumers should verify the exposer contract’s implementation before trusting its attestations.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), Skeletor Spaceman (@skeletor-spaceman), "ERC-8180: Blob Authenticated Messaging [DRAFT]," Ethereum Improvement Proposals, no. 8180, February 2026. Available: https://eips.ethereum.org/EIPS/eip-8180.