--- 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).