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 (extends IERC_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-chainSupporting definitions:
IERC_BAM_Decoder: on-chain contract interface for decoding message payloads (untrusted)keccak256(abi.encodePacked(author, nonce, contentHash))keccak256(abi.encodePacked(sender, nonce, contents)) — standardized input to
the domain-separated signed hashkeccak256(abi.encodePacked("ERC-BAM.v1", chainId))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:
This ERC does NOT standardize:
expose() function signature (varies by proof type)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.
[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.
| 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. |
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);
}
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.declareBlobSegment before emitting BlobBatchRegistered, because
the versioned hash returned by declareBlobSegment is a required field in the event.registerBlobBatch MUST emit BlobBatchRegistered with the versioned hash returned by
declareBlobSegment, msg.sender, the decoder address, and the signature registry address.registerBlobBatch MUST return the versioned hash.registerCalldataBatch MUST compute contentHash as keccak256(batchData).registerCalldataBatch MUST emit CalldataBatchRegistered with the content hash, msg.sender,
the decoder address, and the signature registry address.registerCalldataBatch MUST return the content hash.address(0) is permitted but NOT RECOMMENDED. It indicates no on-chain
decoder is available for the batch, weakening the "anyone can read" property.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.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.
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);
}
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.decode MUST return an empty array and empty bytes for an empty payload.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).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.
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.
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);
}
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 |
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.register MUST revert with AlreadyRegistered if the address already has a registered key.register MUST revert with InvalidProofOfPossession if the proof is invalid.register MUST revert with InvalidPublicKey if the key format is invalid.register MUST emit KeyRegistered with the owner, public key, and assigned index.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).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.supportsAggregation MUST return true if the scheme supports signature aggregation (e.g.,
BLS), false otherwise (e.g., ECDSA).verifyAggregated MUST revert if supportsAggregation() returns false.VerificationFailed error is available for implementation-specific methods (e.g., expose
functions) that require verification to succeed rather than returning a boolean.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.
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);
}
MessageExposed with the content hash, message ID, author, msg.sender, and
uint64(block.timestamp).messageId MUST be computed as keccak256(abi.encodePacked(author, nonce, contentHash)).AlreadyExposed if a message is
exposed twice.isExposed MUST return true if a MessageExposed event has been emitted for the given
messageId by this contract, false otherwise.NotRegistered if not.expose() is not standardizedDifferent signature schemes and proof types require fundamentally different parameters:
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 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 batchesThe result is a globally unique, deterministic identifier per message. The nonce prevents collisions when an author publishes multiple messages in the same batch.
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).
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.
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
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
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
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.
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 |
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.
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:
BlobBatchRegistered events for the decoder address, signature registry address, and
versioned hash.decoder.decode(payload) to extract messages and signature data.messageHash and signedHash for each message (standardized formula).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.
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.
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.
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:
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.
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.
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.
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.
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:
IERC_BAM_Core directly (includes IERC_BSS by inheritance)The BLOBHASH opcode (EIP-4844) is required for registerBlobBatch. The calldata
path (registerCalldataBatch) works on any EVM chain.
| 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) |
| 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 |
| 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 |
| 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 |
| 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) |
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:
BLSRegistry.sol (implements ISignatureRegistry, equivalent to
IERC_BAM_SignatureRegistry, for BLS12-381 with key rotation and revocation extensions)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 |
// 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
);
}
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Different exposers have different trust assumptions. A KZG-based exposer provides cryptographic
proof that a message was in a blob. A merkle-based exposer provides proof against a merkle root. The
MessageExposed event does not indicate the proof type; consumers should verify the exposer
contract's implementation before trusting its attestations.
Copyright and related rights waived via CC0.