Abstract
This ERC defines interfaces for decentralized authenticated messaging over EIP-4844 blobs. The core registration interface extends ERC-8179 (Blob Space Segments) with a decoder pointer and a signature registry pointer: the decoder is an untrusted on-chain contract that extracts messages from payloads; the signature registry is a trusted contract that verifies signatures. Separating decoding from verification ensures that a buggy or malicious decoder cannot cause impersonation — it can only produce wrong messages that fail signature verification against the trusted registry.
Three interfaces:
IERC_BAM_Core(extendsIERC_BSS): register message batches from blobs or calldata with segment coordinates, decoder address, and signature registry addressIERC_BAM_SignatureRegistry: generic registry for managing public keys across multiple cryptographic schemes (ECDSA, BLS, STARK, etc.) with registration, verification, and aggregation supportIERC_BAM_Exposer: standardized event and query interface for proving individual messages from registered batches on-chain
Supporting definitions:
IERC_BAM_Decoder: on-chain contract interface for decoding message payloads (untrusted)- Message ID:
keccak256(abi.encodePacked(author, nonce, contentHash)) - Message hash:
keccak256(abi.encodePacked(sender, nonce, contents))— standardized input to the domain-separated signed hash - Signing domain:
keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
Motivation
EIP-4844 blobs provide 128 KiB of data availability per blob at a fraction of calldata cost. With dictionary-based compression, a single blob holds thousands of messages. Empirical analysis shows capacity exceeding 498 million messages per day. Beyond social messaging, this ERC demonstrates that EIP-4844 blob data is a viable low-cost transport for any signed off-chain message batch.
No standard exists for blob-based messaging. Existing approaches are either minimal and blob-unaware (ERC-3722 Poster, stagnant), NFT-based (ERC-7847, no blob awareness), or L2-specific (Farcaster on Optimism, Lens on zkSync). None standardize the on-chain interfaces for blob-based messaging.
Without a standard, each implementation defines its own batch registration events, key management contracts, message encoding, and exposure mechanisms. Indexers, wallets, and clients cannot interoperate across implementations.
Two design principles guide this ERC:
Anyone can read. A client with an Ethereum node and access to blob data should decode messages from any compliant implementation. The batch registration event contains a decoder address pointing to an on-chain contract that extracts messages from the payload. No dependency on implementation-specific off-chain decoders, proprietary APIs, or centralized indexers.
Capture-minimizing. No privileged decoders, registries, or gatekeepers. Decoder contracts are permissionless to deploy. The decoder address is a per-submission parameter, not a global constant. Implementations choose their own decoders. If a decoder has a bug, deploy a new one; old registrations reference the old decoder, new registrations reference the new one.
This ERC standardizes:
- Batch registration with segment coordinates, decoder pointer, and signature registry pointer (core extends ERC-8179)
- Decoder contract interface for message extraction
- Signature scheme registries for managing public keys across multiple cryptographic schemes (ECDSA, BLS, STARK, etc.)
- Standardized message hash for trustless client-side verification
- Message exposure events for on-chain proofs
This ERC does NOT standardize:
- Message byte layout, batch format, or compression algorithm (decoder-specific)
- Aggregator protocol, blob data archival (beyond EIP-4844’s ~18-day pruning window), or fee splitting
- Social features (follows, likes, profiles, threads)
- The
expose()function signature (varies by proof type)
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Architecture Overview
[Blob/calldata] → registerBlobBatch/registerCalldataBatch (Core)
↓ emits BlobBatchRegistered(decoder, registry)
[Indexer sees event] → decoder.decode(payload) → messages + signatureData
↓
[Client computes] → messageHash per message (standardized formula)
↓
[Client verifies] → registry.verify(pubKey, signedHash, sig) → true/false
↓ (optional, for on-chain reactions)
[Anyone calls] → exposer.expose(params) → emits MessageExposed
The protocol has four components. The core contract is the single on-chain entry point: it registers batches and emits events. The decoder is an untrusted contract that extracts messages from payloads. The signature registry is a trusted contract that verifies signatures for a specific cryptographic scheme. The exposer proves individual messages on-chain for smart contract consumption.
Definitions
| Term | Definition |
|---|---|
| Decoder | An untrusted on-chain contract that extracts messages and signature data from payloads. Implements IERC_BAM_Decoder. |
| Batch | A collection of messages packed into a blob segment or calldata payload. Encoding is decoder-specific. |
| Message | A single message within a batch, containing a sender address, a per-sender nonce, and content bytes. |
| Message ID | keccak256(abi.encodePacked(author, nonce, contentHash)). Unique per message. |
| Message hash | keccak256(abi.encodePacked(sender, nonce, contents)). Standardized hash of a single message. Input to the domain-separated signed hash. |
| Content hash | Identifier for a batch: EIP-4844 versioned hash (blobs) or keccak256(batchData) (calldata). |
| Signing domain | keccak256(abi.encodePacked("ERC-BAM.v1", chainId)). Domain separator for message signatures. |
| Signature scheme | A cryptographic signing algorithm (ECDSA, BLS, STARK, etc.) identified by a 1-byte scheme ID. |
| Proof of possession | A signature proving the registrant controls the private key, preventing rogue key attacks. |
| Aggregated signature | A single signature combining N individual signatures (e.g., BLS aggregation). |
| Exposure | The act of proving a specific message exists within a registered batch and recording that proof on-chain. |
| Exposer | A contract that implements exposure logic for a specific signature scheme and proof type. |
Core Registration Interface (IERC_BAM_Core)
The core contract is the protocol’s single on-chain registration point. Aggregators and
self-publishing users call registerBlobBatch or registerCalldataBatch to declare that a batch
exists. The core stores nothing — it emits events that indexers use to discover batches.
Every compliant core contract MUST implement the IERC_BAM_Core interface, which extends
ERC-8179 (IERC_BSS):
interface IERC_BAM_Core is IERC_BSS {
/// @notice Emitted when a blob batch is registered.
/// @param versionedHash The EIP-4844 versioned hash of the blob.
/// @param submitter The address that registered the batch (msg.sender).
/// @param decoder Decoder contract for extracting messages from the batch payload.
/// @param signatureRegistry Signature registry for verifying message signatures.
event BlobBatchRegistered(
bytes32 indexed versionedHash,
address indexed submitter,
address indexed decoder,
address signatureRegistry
);
/// @notice Emitted when a calldata batch is registered.
/// @param contentHash Content hash (keccak256 of batch data).
/// @param submitter The address that registered the batch (msg.sender).
/// @param decoder Decoder contract for extracting messages from the batch payload.
/// @param signatureRegistry Signature registry for verifying message signatures.
event CalldataBatchRegistered(
bytes32 indexed contentHash,
address indexed submitter,
address indexed decoder,
address signatureRegistry
);
/// @notice Register a blob batch with segment coordinates, decoder, and signature registry.
/// @param blobIndex Index of the blob within the transaction (0-based).
/// @param startFE Start field element (inclusive). MUST be < endFE.
/// @param endFE End field element (exclusive). MUST be <= 4096.
/// @param contentTag Protocol/content identifier (passed to declareBlobSegment).
/// @param decoder Decoder contract address for extracting messages.
/// @param signatureRegistry Signature registry address for verifying message signatures.
/// @return versionedHash The EIP-4844 versioned hash of the blob.
function registerBlobBatch(
uint256 blobIndex,
uint16 startFE,
uint16 endFE,
bytes32 contentTag,
address decoder,
address signatureRegistry
) external returns (bytes32 versionedHash);
/// @notice Register a batch submitted via calldata.
/// @param batchData The batch payload bytes.
/// @param decoder Decoder contract address for extracting messages.
/// @param signatureRegistry Signature registry address for verifying message signatures.
/// @return contentHash The keccak256 hash of batchData.
function registerCalldataBatch(
bytes calldata batchData,
address decoder,
address signatureRegistry
) external returns (bytes32 contentHash);
}
Behavior
registerBlobBatchMUST call the inheriteddeclareBlobSegment(blobIndex, startFE, endFE, contentTag)fromIERC_BSS, which validates segment bounds, retrieves the versioned hash viaBLOBHASH, and emitsBlobSegmentDeclared. IfdeclareBlobSegmentreverts (invalid segment or no blob at index),registerBlobBatchMUST propagate the revert.- Implementations MUST call
declareBlobSegmentbefore emittingBlobBatchRegistered, because the versioned hash returned bydeclareBlobSegmentis a required field in the event. registerBlobBatchMUST emitBlobBatchRegisteredwith the versioned hash returned bydeclareBlobSegment,msg.sender, the decoder address, and the signature registry address.registerBlobBatchMUST return the versioned hash.registerCalldataBatchMUST computecontentHashaskeccak256(batchData).registerCalldataBatchMUST emitCalldataBatchRegisteredwith the content hash,msg.sender, the decoder address, and the signature registry address.registerCalldataBatchMUST return the content hash.- Core implementations MUST NOT write to storage. The event log is the sole record.
- Both functions MUST be permissionless: any address MAY call them.
- 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. - A
signatureRegistryaddress ofaddress(0)is permitted. It indicates the batch is unsigned or uses an off-chain verification mechanism. Clients receivingsignatureRegistry=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. Thesubmitterfield in the event provides batch-level accountability.
Relationship to ERC-8179
Since IERC_BAM_Core extends IERC_BSS, every BAM contract is also a BSS contract.
registerBlobBatch emits both BlobSegmentDeclared (from the inherited declareBlobSegment call)
and BlobBatchRegistered. BSS indexers tracking BlobSegmentDeclared events discover the segment
boundaries. BAM indexers tracking BlobBatchRegistered events discover the batch, its decoder, and
its signature registry.
BAM contracts do not require a shared singleton deployment. Each BAM deployment functions as its own BSS instance. Indexers filter by event topic hash (globally indexed on Ethereum), not by contract address.
For protocols that need BSS without BAM (e.g., non-messaging data in shared blobs), a standalone BSS contract remains the correct choice. BAM is for message batches that benefit from decoder and signature registry discovery.
For calldata batches (registerCalldataBatch), no segment declaration is needed. Calldata has no
blob, no versioned hash, and no field element range. The IERC_BSS extension applies only to the
blob path.
Shared blob segment allocation requires off-chain coordination between parties before the blob transaction is constructed.
Decoder Interface (IERC_BAM_Decoder)
The decoder extracts individual messages and signature data from a raw batch payload. Given raw bytes from a blob segment or calldata batch, a decoder returns the decoded messages and opaque signature data. Decoders are untrusted: a lying decoder produces wrong messages whose hashes fail verification against the trusted registry. Because decoders cannot affect verification outcomes, anyone can deploy one for any encoding format.
interface IERC_BAM_Decoder {
/// @notice A decoded message.
struct Message {
address sender;
uint64 nonce;
bytes contents;
}
/// @notice Decodes all messages and extracts signature data from the payload.
/// @param payload Raw message batch bytes.
/// @return messages Array of decoded messages (sender + nonce + contents).
/// @return signatureData Opaque signature bytes (e.g., aggregated BLS signature,
/// concatenated ECDSA signatures). Format depends on the
/// signature scheme; length is derivable from
/// signatureRegistry.signatureSize() and message count.
function decode(bytes calldata payload)
external view returns (Message[] memory messages, bytes memory signatureData);
}
Behavior
decodeMUST return all messages in the payload as an array ofMessagestructs. Each message contains the sender’s Ethereum address, a per-sender monotonically increasing nonce, and the content bytes.decodeMUST return an empty array and empty bytes for an empty payload.decodeMUST 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).- Decoder behavior MUST be deterministic: given the same payload, repeated calls MUST return the same result.
- Decoder contracts MAY read external state (e.g., shared compression dictionaries) but MUST NOT produce side effects.
- Decoders MUST NOT perform signature verification. Verification is the client’s responsibility using the trusted registry.
Decoder design guidance
v1 decoders SHOULD use simple encodings: ABI-encoded arrays, RLP, or SSZ. At minimum, a v1 decoder
extracts a list of (sender: address, nonce: uint64, contents: bytes) tuples and appended signature
data. Complex compression (dictionary-based, delta encoding) can be introduced in later decoder
versions. The IERC_BAM_Decoder interface is encoding-agnostic; a v2 decoder with on-chain
decompression implements the same function.
Nonce Semantics
Nonces MUST be per-sender monotonically increasing within the signing protocol. A decoder MAY return messages with non-sequential nonces. Clients SHOULD treat messages whose nonce does not exceed the last-accepted nonce for that sender as invalid.
If a decoder returns duplicate (sender, nonce) pairs, the resulting message IDs collide. Clients
MUST de-duplicate by messageId.
Nonce correctness is enforced by the signing protocol: the signer includes the correct nonce in the
signed hash. An incorrect nonce produces a different messageHash, causing signature verification to
fail.
Signature Registry Interface (IERC_BAM_SignatureRegistry)
The signature registry maps Ethereum addresses to public keys and provides signature verification for a specific cryptographic scheme (ECDSA, BLS, STARK, etc.). One registry per scheme is expected — roughly four for the foreseeable future.
Every compliant signature registry MUST implement the IERC_BAM_SignatureRegistry interface:
interface IERC_BAM_SignatureRegistry {
event KeyRegistered(address indexed owner, bytes pubKey, uint256 index);
error AlreadyRegistered(address owner);
error NotRegistered(address owner);
error InvalidProofOfPossession();
error InvalidPublicKey();
error InvalidSignature();
error VerificationFailed();
// Scheme identification
function schemeId() external pure returns (uint8 id);
function schemeName() external pure returns (string memory name);
function pubKeySize() external pure returns (uint256 size);
function signatureSize() external pure returns (uint256 size);
// Registration
function register(bytes calldata pubKey, bytes calldata popProof)
external returns (uint256 index);
function getKey(address owner)
external view returns (bytes memory pubKey);
function isRegistered(address owner)
external view returns (bool registered);
// Verification
function verify(
bytes calldata pubKey,
bytes32 messageHash,
bytes calldata signature
) external view returns (bool valid);
function verifyWithRegisteredKey(
address owner,
bytes32 messageHash,
bytes calldata signature
) external view returns (bool valid);
// Aggregation
function supportsAggregation() external pure returns (bool supported);
function verifyAggregated(
bytes[] calldata pubKeys,
bytes32[] calldata messageHashes,
bytes calldata aggregatedSignature
) external view returns (bool valid);
}
Behavior
-
schemeId()MUST return a unique 1-byte identifier for the signature scheme. Assigned IDs:ID Scheme 0x01ECDSA-secp256k1 0x02BLS12-381 0x03STARK-Poseidon 0x04Dilithium 0x05-0xFFReserved -
registerMUST 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. -
registerMUST revert withAlreadyRegisteredif the address already has a registered key. -
registerMUST revert withInvalidProofOfPossessionif the proof is invalid. -
registerMUST revert withInvalidPublicKeyif the key format is invalid. -
registerMUST emitKeyRegisteredwith the owner, public key, and assigned index. -
verifyMUST returntrueif the signature is valid for the given public key and message hash,falseotherwise. ThemessageHashparameter is the final hash that was signed (after domain separation). Domain separation is the caller’s responsibility; the registry is domain-unaware.verifyMUST NOT revert on invalid signatures. It MAY revert withInvalidSignatureif the signature bytes are malformed (e.g., wrong length for the scheme). -
verifyWithRegisteredKeyMUST revert withNotRegisteredif the owner has no registered key. Otherwise, it MUST behave identically toverifyusing the owner’s registered key. -
supportsAggregationMUST returntrueif the scheme supports signature aggregation (e.g., BLS),falseotherwise (e.g., ECDSA). -
verifyAggregatedMUST revert ifsupportsAggregation()returnsfalse. -
The
VerificationFailederror is available for implementation-specific methods (e.g., expose functions) that require verification to succeed rather than returning a boolean. -
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.
-
The base interface does not prevent two addresses from registering the same public key. Both would pass proof-of-possession (proving they hold the private key). Registries MAY enforce key uniqueness; if they do not, signature verification is ambiguous for shared keys. This is the registrant’s responsibility.
For BLS-based registries, every sender whose messages appear in a batch MUST have a registered public key before those messages can be verified or exposed. ECDSA-based registries MAY allow keyless verification via ecrecover-style key derivation.
Message Exposure Interface (IERC_BAM_Exposer)
The exposer proves on-chain that a specific message exists in a registered batch and that its signature is valid. This enables on-chain contracts to react to specific messages (governance, token gates, dispute resolution).
Every compliant exposer contract MUST implement the IERC_BAM_Exposer interface:
interface IERC_BAM_Exposer {
event MessageExposed(
bytes32 indexed contentHash,
bytes32 indexed messageId,
address indexed author,
address exposer,
uint64 timestamp
);
error NotRegistered(bytes32 contentHash);
error AlreadyExposed(bytes32 messageId);
function isExposed(bytes32 messageId) external view returns (bool exposed);
}
Behavior
- When an implementation’s expose function successfully verifies and records a message, it MUST
emit
MessageExposedwith the content hash, message ID, author,msg.sender, anduint64(block.timestamp). - The
messageIdMUST be computed askeccak256(abi.encodePacked(author, nonce, contentHash)). - Implementations MUST track exposed message IDs and revert with
AlreadyExposedif a message is exposed twice. isExposedMUST returntrueif aMessageExposedevent has been emitted for the givenmessageIdby this contract,falseotherwise.- Implementations SHOULD verify that the content hash corresponds to a registered batch (via the
core contract) and revert with
NotRegisteredif not. - 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:
authoris the message author’s Ethereum address (address, 20 bytes)nonceis a per-author monotonically increasing counter (uint64, 8 bytes)contentHashis the batch identifier (bytes32, 32 bytes): versioned hash for blob batches,keccak256(batchData)for calldata batches
The result is a globally unique, deterministic identifier per message. The nonce prevents collisions when an author publishes multiple messages in the same batch.
Signing Domain and Message Hash Convention
Message hashes MUST be computed as:
messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
Where sender is the message author’s Ethereum address (address, 20 bytes), nonce is the
per-sender monotonically increasing counter (uint64, 8 bytes), and contents is the message content (bytes,
variable length).
Message signatures MUST use a domain separator to prevent cross-chain replay:
domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
Where chainId is the EIP-155 chain ID (uint256). The signed message hash is then:
signedHash = keccak256(abi.encodePacked(domain, messageHash))
The standardized messageHash formula enables trustless verification: a client computes hashes from
the decoder’s output and verifies them against the trusted registry. If the decoder lies about
message contents, the client computes wrong hashes that fail signature verification. The decoder can
cause false negatives (valid messages rejected) but never false positives (forged messages
accepted).
Worked Examples
Example 1: Aggregator Submits a Blob Batch
An aggregator collects 500 messages, encodes them using a v1 decoder format, packs the payload into a blob, and submits:
Transaction:
1. Submit blob (type-3 tx with 1 blob)
2. core.registerBlobBatch(
0, 0, 4096, keccak256("social-blobs.v4"), decoderAddr, sigRegistryAddr
)
→ declareBlobSegment(0, 0, 4096, keccak256("social-blobs.v4"))
→ emits BlobSegmentDeclared(vHash, aggregator, 0, 4096, contentTag)
→ emits BlobBatchRegistered(vHash, aggregator, decoderAddr, sigRegistryAddr)
Client verification (by anyone):
1. DECODE (untrusted)
- See BlobBatchRegistered → get versionedHash, decoderAddr, sigRegistryAddr
- Fetch blob data via versioned hash
- (messages, signatureData) = decoder.decode(blobData)
→ 500 messages with sender, nonce, contents
2. COMPUTE HASHES (client, standardized)
- domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
- for each message:
messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
signedHash = keccak256(abi.encodePacked(domain, messageHash))
3. VERIFY (trusted registry)
- pubKeys = [registry.getKey(m.sender) for m in messages]
- registry.verifyAggregated(pubKeys, signedHashes, signatureData) → true
Gas: ~21,000 (intrinsic) + ~3,500 (declareBlobSegment) + ~2,400 (BlobBatchRegistered event) + blob gas. Total BAM overhead: ~5,900 gas.
Example 2: User Self-Publishes via Calldata
A user bypasses aggregators and publishes a single-message batch:
Transaction:
1. core.registerCalldataBatch(batchData, decoderAddr, sigRegistryAddr)
→ computes contentHash = keccak256(batchData)
→ emits CalldataBatchRegistered(
contentHash, user, decoderAddr, sigRegistryAddr
)
Client verification:
1. DECODE
- See CalldataBatchRegistered → get calldata from tx, decoderAddr, sigRegistryAddr
- (messages, signatureData) = decoder.decode(batchData)
→ 1 message
2. COMPUTE HASHES
- domain = keccak256(abi.encodePacked("ERC-BAM.v1", chainId))
- messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
- signedHash = keccak256(abi.encodePacked(domain, messageHash))
3. VERIFY
- pubKey = registry.getKey(message.sender)
- registry.verify(pubKey, signedHash, signatureData) → true
Example 3: Shared Blob with L2 Rollup
An aggregator shares a blob with a rollup. The rollup uses field elements 0-1999; the messaging protocol uses 2000-4095. Shared blob segment allocation requires off-chain coordination between parties before the blob transaction is constructed.
Transaction:
1. rollup.submitBatch(...) // L2 data
2. bss.declareBlobSegment(0, 0, 2000, keccak256("optimism.bedrock")) // standalone BSS
3. core.registerBlobBatch( // BAM (extends BSS)
0, 2000, 4096, keccak256("social-blobs.v4"), decoderAddr, sigRegistryAddr
)
→ emits BlobSegmentDeclared(vHash, aggregator, 2000, 4096, ...)
→ emits BlobBatchRegistered(vHash, aggregator, decoderAddr, sigRegistryAddr)
Events:
- BlobSegmentDeclared [0, 2000) "optimism.bedrock" (standalone BSS)
- BlobSegmentDeclared [2000, 4096) "social-blobs.v4" (BAM contract)
- BlobBatchRegistered (decoderAddr, sigRegistryAddr) (BAM contract)
Client verification:
1. See BlobBatchRegistered → know FE range from BlobSegmentDeclared on same contract
2. Fetch blob, read FE [2000, 4096)
3. (messages, signatureData) = decoder.decode(segmentData)
4. Compute messageHash and signedHash for each message (standardized)
5. registry.verifyAggregated(pubKeys, signedHashes, signatureData) → true
Example 4: Key Registration and Message Exposure
A user registers a BLS key; later a message is exposed on-chain:
Setup:
1. blsRegistry.register(blsPubKey, popSignature)
→ emits KeyRegistered(user, blsPubKey, index)
Exposure (by anyone, permissionless):
2. exposer.expose(params) // implementation-specific function
→ decodes message via decoder
→ computes messageHash = keccak256(abi.encodePacked(sender, nonce, contents))
→ computes signedHash = keccak256(abi.encodePacked(domain, messageHash))
→ verifies BLS signature against registered key via registry
→ verifies KZG proof against versioned hash
→ emits MessageExposed(contentHash, messageId, author, exposer, timestamp)
Query:
3. exposer.isExposed(messageId) → true
Rationale
BAM extends BSS
The original design defined registerBlobBatch(blobIndex) with no segment coordinates. When sharing
blobs with other protocols, callers needed a separate ERC-8179 call to declare their
segment. Two calls to two contracts created a correlation problem: a shared blob with N segments
produces N BlobSegmentDeclared events and one BlobBatchRegistered event, all referencing the
same versioned hash. No on-chain link connected the BAM batch to its specific BSS segment.
Inheriting IERC_BSS eliminates the ambiguity. registerBlobBatch declares the segment and
registers the batch atomically: one call, two events, unambiguous correlation.
BAM contracts do not require a singleton deployment. Each deployment emits BlobSegmentDeclared
from its own address. Ethereum event topics are globally indexed; indexers filter by topic hash, not
by contract address. The singleton pattern in BSS was a simplicity choice, not a requirement.
Trust separation: decoder vs registry
The original design bundled decoding and verification in a single “schema” contract. If a client
trusts a bad schema, anyone can impersonate any address (the schema’s verify could always return
true). Separating decoding (untrusted, permissionless) from verification (trusted, few instances)
eliminates this risk.
Decoders are “open permissionless innovation” — many exist, anyone can deploy one, and a buggy decoder causes only false negatives (valid messages rejected), never false positives (forged messages accepted). Registries are “mostly ~4 highly-audited instances” — one per signature scheme. The trust surface is narrow and auditable.
The standardized messageHash = keccak256(abi.encodePacked(sender, nonce, contents)) formula is the bridge: the
client computes hashes from the decoder’s (untrusted) output, then verifies them against the
registry’s (trusted) verification. If the decoder lies, the hashes are wrong, and verification
fails.
| Component | Trust | Count | Risk of lying |
|---|---|---|---|
| Decoder | Untrusted | Many | Low — wrong output fails signature verification |
| Registry | Trusted | ~4 | High — wrong verification enables impersonation |
On-chain decoder discovery
A decoder contract is a Solidity contract deployed on-chain that extracts messages and signature
data from payloads. Given raw bytes, it returns an array of Message structs (sender + nonce +
contents) and opaque signature bytes. The decoder address is emitted in the registration event,
discoverable from the event log alone.
Traditional approaches embed decoding logic in off-chain clients. If a protocol changes its encoding, every client needs an update. On-chain decoders invert this: the decoder is on-chain, auditable, and callable by any contract or client.
v1 decoders use simple encodings (ABI, RLP, or SSZ). Complex compression (zstd with shared
dictionaries, delta encoding) can be introduced in later decoder versions. The IERC_BAM_Decoder
interface is encoding-agnostic; decoder upgrades do not change the interface.
”Anyone can read”
A client that (a) has access to an Execution Layer node (for event logs and transaction data) and (b) has access to a Consensus Layer node or blob archival service (for raw blob data) decodes and verifies messages from any BAM-compliant implementation:
- Scan
BlobBatchRegisteredevents for the decoder address, signature registry address, and versioned hash. - Fetch blob data via the versioned hash.
- Call
decoder.decode(payload)to extract messages and signature data. - Compute
messageHashandsignedHashfor each message (standardized formula). - Call
registry.verifyAggregated(pubKeys, signedHashes, signatureData)to validate.
No dependency on implementation-specific indexers, aggregators, or off-chain APIs. The on-chain
decoder is the canonical extractor. Proprietary encodings without on-chain decoders are permitted
(decoder = address(0)) but create centralization pressure: users depend on the protocol’s
off-chain decoder, which is a capture vector.
Capture-minimizing design
Decoder contracts are permissionless to deploy. The decoder address is a parameter in
registerBlobBatch, not a value read from a registry. No governance, no approval, no gatekeeping.
Different implementations use different decoders. Different versions of the same implementation use
different decoders. Nothing prevents forking a decoder contract and deploying a modified version.
Zero-storage core
The core contract emits events and stores nothing. The same rationale applies to
ERC-3722 (Poster) and ERC-8179: the event log suffices for
indexing, and avoiding SSTORE keeps registration costs minimal.
registerBlobBatch costs approximately 5,900 gas (segment validation + two event emissions).
Gas estimates are approximate, based on Cancun EVM pricing, and verified against the reference
implementation’s forge benchmarks. Message registration executes alongside blob transactions
costing 21,000+ gas base plus blob gas. Under 6,000 gas overhead adds under 29% to the cheapest
possible blob transaction.
Exposure as a separate contract
The core contract registers data without interpreting it. Exposure (proving a specific message exists in a batch) requires signature verification, proof validation, and scheme-specific logic. Combining registration and exposure in one contract couples proof-type support to batch registration, forcing all implementations to support the same verification mechanisms.
Separating core and exposer allows:
- One core contract serving multiple exposers (BLS exposer, ECDSA exposer, ZK exposer)
- Exposer upgrades without touching the core
- Different trust models (core is trustless; exposers may have scheme-specific assumptions)
Why the expose function is not standardized
BLS+KZG requires different parameters than ECDSA+Merkle or STARK+ZK. Forcing one function signature
would either lose type safety or exclude future proof types. The event provides the interoperability
surface: any exposer, regardless of proof mechanism, emits MessageExposed. Smart contracts and
indexers react to the event, not the function.
Generic signature registry
A single IERC_BAM_SignatureRegistry interface works across ECDSA, BLS, STARK, and future schemes.
This avoids N separate standards for N schemes. The schemeId byte and supportsAggregation flag
are the only scheme-specific metadata; everything else (register, verify, getKey) is uniform.
BLS12-381 is the primary use case today (signature aggregation saves 79-94% of authentication overhead depending on batch size), but the interface supports post-quantum schemes (Dilithium) and ZK-friendly schemes (STARK-Poseidon) without modification.
The signature registry interface is reusable by any protocol needing on-chain key management and multi-scheme signature verification. Future ERCs may adopt or extend this interface as a standalone registry standard.
Message ID determinism
keccak256(abi.encodePacked(author, nonce, contentHash)) is deterministic and computable from the message data
alone, requiring no on-chain state. The author address prevents cross-user collisions, the nonce
prevents same-batch collisions, and the content hash binds the ID to a specific batch.
Domain separator for signing
The "ERC-BAM.v1" prefix prevents signature reuse across protocols; chainId prevents cross-chain
replay. For individual user self-publication with ECDSA, adopters may define an EIP-712 TypedData
struct matching the messageHash fields for improved wallet display. The core standard does not
mandate EIP-712 because aggregated BLS signing (the primary blob path) uses headless signing where
wallet display provides no benefit.
Backwards Compatibility
This ERC introduces new interfaces and does not modify any existing standards.
Existing messaging contracts (e.g., ERC-3722 Poster) can adopt this ERC by:
- Implementing
IERC_BAM_Coredirectly (includesIERC_BSSby inheritance) - Deploying a standalone core contract and calling it within the same transaction
The BLOBHASH opcode (EIP-4844) is required for registerBlobBatch. The calldata
path (registerCalldataBatch) works on any EVM chain.
Test Cases
Core Registration
| Function | Input | Expected Result |
|---|---|---|
registerBlobBatch(0, 0, 4096, tag, decoder, sigReg) | Blob at index 0, full blob | Emits BlobSegmentDeclared + BlobBatchRegistered, returns versioned hash |
registerBlobBatch(99, 0, 4096, tag, decoder, sigReg) | No blob at index 99 | Reverts NoBlobAtIndex(99) |
registerBlobBatch(0, 4096, 0, tag, decoder, sigReg) | Invalid segment | Reverts InvalidSegment(4096, 0) |
registerBlobBatch(0, 0, 5000, tag, decoder, sigReg) | endFE out of range | Reverts InvalidSegment(0, 5000) |
registerCalldataBatch(data, decoder, sigReg) | 1,000 bytes of batch data | Emits CalldataBatchRegistered with keccak256 hash |
registerCalldataBatch(data, address(0), sigReg) | No decoder | Emits CalldataBatchRegistered with decoder=address(0) |
Decoder
| Function | Input | Expected Result |
|---|---|---|
decode(payload) | 500-message batch | Returns 500 Message structs + aggregated signature bytes |
decode(empty) | Empty payload | Returns empty array + empty bytes |
decode(payload) | Valid BLS batch | Returns messages and 96-byte aggregated BLS signature |
decode(payload) | Valid ECDSA batch | Returns messages and N*65-byte concatenated ECDSA signatures |
Signature Registry
| Function | Input | Expected Result |
|---|---|---|
schemeId | BLS registry | Returns 0x02 |
schemeName | BLS registry | Returns "BLS12-381" |
pubKeySize | BLS registry | Returns 48 |
signatureSize | BLS registry | Returns 96 |
register | Valid BLS key + PoP | Emits KeyRegistered, returns index |
register | Already registered address | Reverts AlreadyRegistered |
register | Invalid PoP signature | Reverts InvalidProofOfPossession |
register | Malformed public key | Reverts InvalidPublicKey |
getKey | Registered address | Returns the registered public key |
getKey | Unregistered address | Returns empty bytes |
isRegistered | Registered address | Returns true |
isRegistered | Unregistered address | Returns false |
verify | Valid signature | Returns true |
verify | Invalid signature | Returns false |
verifyWithRegisteredKey | Registered owner, valid sig | Returns true |
verifyWithRegisteredKey | Unregistered owner | Reverts NotRegistered |
supportsAggregation | BLS registry | Returns true |
supportsAggregation | ECDSA registry | Returns false |
verifyAggregated | Valid aggregated BLS sig | Returns true |
verifyAggregated | ECDSA registry (no agg) | Reverts |
Message Exposure
| Function | Input | Expected Result |
|---|---|---|
isExposed | Unexposed message ID | Returns false |
isExposed | Exposed message ID | Returns true |
| Expose call | Valid proof + signature | Emits MessageExposed, returns message ID |
| Expose call | Unregistered batch | Reverts NotRegistered |
| Expose call | Already exposed message | Reverts AlreadyExposed |
Message ID
| Author (address) | Nonce | Content Hash | Expected Message ID |
|---|---|---|---|
0xABCD...0001 | 0 | 0x1234...5678 | keccak256(abi.encodePacked(author, 0, hash)) |
0xABCD...0001 | 1 | 0x1234...5678 | Different from nonce=0 (same batch, different ID) |
Reference Implementation
A reference implementation exists for the signature registry and exposer interfaces. The existing contracts predate the decoder/signature-registry separation and BSS-extension features of this ERC and use protocol-specific naming; they are functionally equivalent to the standardized interfaces for signature registry and exposure:
- Signature Registry:
BLSRegistry.sol(implementsISignatureRegistry, equivalent toIERC_BAM_SignatureRegistry, for BLS12-381 with key rotation and revocation extensions) - Exposer:
BLSExposer.sol(functionally equivalent toIERC_BAM_Exposerwith KZG point evaluation proofs and BLS signature verification)
Updating the reference contracts to implement the ERC interfaces directly (with ERC naming) is tracked as a separate task.
Deployed on Sepolia:
| Contract | Address |
|---|---|
| SocialBlobsCore | 0xAdd498490f0Ffc1ba15af01D6Bf6374518fE0969 |
| BLSRegistry | 0x2146758C8f24e9A0aFf98dF3Da54eef9f53BCFbf |
| BLSExposer | 0x0136454b435fE6cCa5F7b8A6a8cFB5B549afB717 |
Minimal Core Implementation
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;
import {IERC_BAM_Core} from "./IERC_BAM_Core.sol";
contract BlobAuthenticatedMessagingCore is IERC_BAM_Core {
uint16 internal constant MAX_FIELD_ELEMENTS = 4096;
/// @inheritdoc IERC_BSS
function declareBlobSegment(
uint256 blobIndex,
uint16 startFE,
uint16 endFE,
bytes32 contentTag
) public returns (bytes32 versionedHash) {
if (startFE >= endFE || endFE > MAX_FIELD_ELEMENTS) {
revert InvalidSegment(startFE, endFE);
}
assembly {
versionedHash := blobhash(blobIndex)
}
if (versionedHash == bytes32(0)) revert NoBlobAtIndex(blobIndex);
emit BlobSegmentDeclared(versionedHash, msg.sender, startFE, endFE, contentTag);
}
/// @inheritdoc IERC_BAM_Core
function registerBlobBatch(
uint256 blobIndex,
uint16 startFE,
uint16 endFE,
bytes32 contentTag,
address decoder,
address signatureRegistry
) external returns (bytes32 versionedHash) {
versionedHash = declareBlobSegment(blobIndex, startFE, endFE, contentTag);
emit BlobBatchRegistered(
versionedHash, msg.sender, decoder, signatureRegistry
);
}
/// @inheritdoc IERC_BAM_Core
function registerCalldataBatch(
bytes calldata batchData,
address decoder,
address signatureRegistry
) external returns (bytes32 contentHash) {
contentHash = keccak256(batchData);
emit CalldataBatchRegistered(
contentHash, msg.sender, decoder, signatureRegistry
);
}
}
Security Considerations
Segment overlap
Segment overlap — two declarations claiming overlapping field element ranges in the same blob — is
not prevented on-chain. Clients must detect overlap by cross-referencing BlobSegmentDeclared events
sharing the same versioned hash.
Batch registration spam
Registering a blob batch requires a type-3 transaction with at least one blob (~21,000 intrinsic gas plus blob gas fees). Registering a calldata batch costs calldata gas proportional to data size. Both are self-limiting: spam costs the spammer gas without affecting other users. The core contract stores nothing, so spam events increase log volume but not state bloat.
Decoder trust model
A decoder contract is user-deployed code. It may contain bugs, return incorrect Message structs,
or consume excessive gas. However, because decoders do not verify signatures, a buggy decoder cannot
cause impersonation. If a decoder returns wrong messages, the client computes wrong hashes that fail
verification against the trusted registry. The worst case is denial of service (valid messages
rejected), not forgery (fake messages accepted).
A decoder behind an upgradeable proxy could change behavior after deployment. This is lower-risk than in the bundled schema design because the decoder cannot affect verification outcomes, but consumers should still verify whether a decoder is immutable for defense in depth.
Registry trust model
Signature registries are the trusted component. A malicious or buggy registry could return incorrect
verification results, enabling impersonation. The number of registries is intentionally small (~one
per signature scheme) to minimize the audit surface. Consumers should verify that the
signatureRegistry address in a BlobBatchRegistered event corresponds to a known, audited
implementation before trusting verification results.
A registry behind an upgradeable proxy is a critical risk: it could be changed to accept any signature. Registries should be deployed as immutable contracts.
Decoder denial of service
A malicious decoder could execute unbounded computation in decode, consuming excessive gas.
On-chain callers (e.g., exposer contracts) should set gas limits when calling decoder functions.
Off-chain callers (indexers, clients) should enforce execution timeouts.
Key squatting in signature registries
A malicious actor could register a key for an address before the legitimate owner. The proof of
possession requirement prevents this: register requires a signature proving the caller controls
the private key corresponding to the public key being registered. An attacker cannot register
someone else’s key without their private key.
Rogue key attacks (aggregation)
BLS signature aggregation is vulnerable to rogue key attacks where a malicious signer crafts a
public key that cancels out honest signers’ contributions. The mandatory proof of possession in
register mitigates this by ensuring every registered key has a corresponding private key holder.
Cross-chain replay
The signing domain convention includes chainId, preventing signatures from being replayed on other
chains. Implementations should use the domain separator when computing signed message hashes.
Message hash and message ID collisions
The message hash is keccak256(abi.encodePacked(sender, nonce, contents)). The abi.encodePacked encoding is
unambiguous because sender (20 bytes) and nonce (8 bytes) are fixed-size, so the variable-length
contents field always begins at byte 28. No two distinct (sender, nonce, contents) tuples
produce the same packed encoding.
The message ID is keccak256(abi.encodePacked(author, nonce, contentHash)). All three fields are fixed-size (20 +
8 + 32 bytes), so the encoding is trivially unambiguous.
For an attacker to find two distinct inputs that produce the same hash for either formula requires a collision attack on keccak256 (birthday bound ~2^128 security). Finding a second input that matches a specific existing hash requires a preimage or second preimage attack (~2^256 security). Both are computationally infeasible.
Exposure replay
The AlreadyExposed error and isExposed query prevent the same message from being exposed twice.
Implementations must maintain a mapping of exposed message IDs. This is the one required storage
operation in the exposure interface.
Content hash binding
BlobBatchRegistered binds a versioned hash to a submitter. The versioned hash is
retrieved via BLOBHASH, which only returns non-zero values for blobs in the current transaction.
An attacker cannot register a batch for someone else’s blob; they would need to include the blob in
their own transaction.
For calldata batches, the content hash is keccak256(batchData), which is deterministic. Anyone can
register the same calldata, but the submitter field distinguishes registrations.
Blob data pruning
EIP-4844 blob data is pruned after ~18 days. Batch registration events persist indefinitely, but the underlying blob data may become unavailable. Implementations should consider archival strategies for blob data preservation. Message exposure creates a permanent on-chain record of individual messages, which survives blob pruning.
Unverified batch content
The core contract registers batches without inspecting their content. A registered batch may contain malformed, empty, or malicious data. Registration is a claim that a batch exists, not a guarantee of its validity. Indexers and exposers must independently validate batch content.
Exposer trust model
Different exposers have different trust assumptions. A KZG-based exposer provides cryptographic
proof that a message was in a blob. A merkle-based exposer provides proof against a merkle root. The
MessageExposed event does not indicate the proof type; consumers should verify the exposer
contract’s implementation before trusting its attestations.
Copyright
Copyright and related rights waived via CC0.