---
eip: 8180
title: Blob Authenticated Messaging
description: Interfaces for authenticated messaging over EIP-4844 blobs with on-chain decoder discovery
author: Vitalik Buterin (@vbuterin), Skeletor Spaceman (@skeletor-spaceman)
discussions-to: https://ethereum-magicians.org/t/erc-8180-blob-authenticated-messaging-bam/27868
status: Draft
type: Standards Track
category: ERC
created: 2026-02-21
requires: 4844, 8179
---

## Abstract

This ERC defines interfaces for decentralized authenticated messaging over [EIP-4844](./eip-4844.md) blobs.
The core registration interface extends [ERC-8179](./eip-8179.md) (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](./eip-3722.md) Poster, stagnant), NFT-based ([ERC-7847](./eip-7847.md), 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](./eip-8179.md))
- 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](./eip-8179.md) (`IERC_BSS`):

```solidity
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.

```solidity
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:

```solidity
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:

```solidity
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](./eip-155.md) 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](./eip-8179.md) 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](./eip-3722.md) (Poster) and [ERC-8179](./eip-8179.md): 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](./eip-712.md) 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](./eip-3722.md) 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](./eip-4844.md)) 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

```solidity
// 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

Copyright and related rights waived via [CC0](../LICENSE.md).
