From 4dcb946e848cc640560a7d16276cd26d16a2667d Mon Sep 17 00:00:00 2001 From: orca <227982352+0xrcinus@users.noreply.github.com> Date: Wed, 20 May 2026 09:28:49 +0000 Subject: [PATCH] Update ERC-8180: promote contentTag to indexed topic on BAM core events Aligns BAM with ERC-8179's first-class filter key: - BlobBatchRegistered: bytes32 indexed contentTag occupies the third indexed topic; decoder demoted to unindexed event data - CalldataBatchRegistered: same layout - registerCalldataBatch gains a bytes32 contentTag parameter - Behavior clauses 12-14: verbatim binding, no-rejection validation, indexed-topic layout (breaking change) - New Rationale subsections: contentTag uniformity across blob and calldata paths, why contentTag is indexed and decoder is not - New Security Considerations: tag spoofing, null-tag discouragement, calldata griefing economics - Test Cases: new rows for contentTag verbatim emission and eth_getLogs filter behavior on both BAM events - Worked Examples and Minimal Core Implementation updated - Sepolia table reflects amended core deployment alongside legacy Breaking change: existing deployments emit the upstream layout; new deployments would emit the amended layout. Acceptable for Draft-status ERC; no on-chain coexistence path is proposed. Co-Authored-By: Claude Opus 4.7 (1M context) --- ERCS/erc-8180.md | 202 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 174 insertions(+), 28 deletions(-) diff --git a/ERCS/erc-8180.md b/ERCS/erc-8180.md index d0133ad2ea1..e2ff2f5efa2 100644 --- a/ERCS/erc-8180.md +++ b/ERCS/erc-8180.md @@ -95,7 +95,8 @@ described in RFC 2119 and RFC 8174. ``` [Blob/calldata] → registerBlobBatch/registerCalldataBatch (Core) - ↓ emits BlobBatchRegistered(decoder, registry) + ↓ emits BlobBatchRegistered(contentTag, decoder, registry) or + ↓ CalldataBatchRegistered(contentTag, decoder, registry) [Indexer sees event] → decoder.decode(payload) → messages + signatureData ↓ [Client computes] → messageHash per message (standardized formula) @@ -142,24 +143,30 @@ 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 contentTag Protocol/content identifier (equal to the `contentTag` + /// argument passed to `registerBlobBatch`). /// @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, + bytes32 indexed contentTag, + address 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 contentTag Protocol/content identifier (equal to the `contentTag` + /// argument passed to `registerCalldataBatch`). /// @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, + bytes32 indexed contentTag, + address decoder, address signatureRegistry ); @@ -167,7 +174,8 @@ interface IERC_BAM_Core is IERC_BSS { /// @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 contentTag Protocol/content identifier (passed to declareBlobSegment + /// and emitted verbatim in `BlobBatchRegistered`). /// @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. @@ -182,11 +190,15 @@ interface IERC_BAM_Core is IERC_BSS { /// @notice Register a batch submitted via calldata. /// @param batchData The batch payload bytes. + /// @param contentTag Protocol/content identifier (emitted verbatim in + /// `CalldataBatchRegistered`). `bytes32(0)` is accepted but + /// NOT RECOMMENDED — see Security Considerations. /// @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, + bytes32 contentTag, address decoder, address signatureRegistry ) external returns (bytes32 contentHash); @@ -203,11 +215,12 @@ interface IERC_BAM_Core is IERC_BSS { 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. + `declareBlobSegment`, `msg.sender`, the caller-supplied `contentTag`, 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. + the caller-supplied `contentTag`, 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. @@ -218,14 +231,37 @@ interface IERC_BAM_Core is IERC_BSS { 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. +12. **`contentTag` binding.** The `contentTag` value emitted in each BAM core event MUST equal the + `contentTag` argument the caller passed to the registration function. Implementations MUST NOT + re-derive, hash, normalize, or default the emitted value. For `registerBlobBatch`, this is the + same value forwarded to `declareBlobSegment` and emitted in `BlobSegmentDeclared`; the + `contentTag` topic MUST therefore be identical across the `BlobSegmentDeclared` and + `BlobBatchRegistered` events produced by a single `registerBlobBatch` call. +13. **`contentTag` validation.** Implementations MUST NOT reject any `contentTag` value. + `bytes32(0)` is accepted at the contract layer (see Security Considerations for why it is NOT + RECOMMENDED at the application layer). A `require(contentTag != 0)` check at either registration + function is non-conforming. +14. **Indexed-topic layout.** In both BAM core events, `contentTag` occupies the third indexed + topic slot (`topic[3]`); `decoder` and `signatureRegistry` are unindexed event data. This layout + lets consumers issue an `eth_getLogs` filter keyed on `contentTag` against either BAM core + event directly, at parity with ERC-8179's `BlobSegmentDeclared`. This is a breaking change + relative to earlier drafts in which `decoder` occupied the third indexed slot. #### 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. +boundaries. BAM indexers tracking `BlobBatchRegistered` events discover the batch, its `contentTag`, +its decoder, and its signature registry. + +Because `contentTag` is now emitted as an indexed topic on both BAM core events, consumers that +want "every BAM batch for protocol X" issue a single `eth_getLogs` filter against +`BlobBatchRegistered` or `CalldataBatchRegistered` keyed on the `contentTag` topic. No same- +transaction log-index correlation against `BlobSegmentDeclared` is required. On shared blobs — +one blob carrying multiple declared segments — this eliminates the pairing ambiguity that earlier +drafts of this ERC exposed: each BAM batch now carries its own `contentTag` directly in the event +log, independent of any other segment declared under the same versioned hash. 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 @@ -524,11 +560,11 @@ Transaction: ) → declareBlobSegment(0, 0, 4096, keccak256("social-blobs.v4")) → emits BlobSegmentDeclared(vHash, aggregator, 0, 4096, contentTag) - → emits BlobBatchRegistered(vHash, aggregator, decoderAddr, sigRegistryAddr) + → emits BlobBatchRegistered(vHash, aggregator, contentTag, decoderAddr, sigRegistryAddr) Client verification (by anyone): 1. DECODE (untrusted) - - See BlobBatchRegistered → get versionedHash, decoderAddr, sigRegistryAddr + - See BlobBatchRegistered → get versionedHash, contentTag, decoderAddr, sigRegistryAddr - Fetch blob data via versioned hash - (messages, signatureData) = decoder.decode(blobData) → 500 messages with sender, nonce, contents @@ -553,15 +589,17 @@ A user bypasses aggregators and publishes a single-message batch: ``` Transaction: - 1. core.registerCalldataBatch(batchData, decoderAddr, sigRegistryAddr) + 1. core.registerCalldataBatch( + batchData, keccak256("social-blobs.v4"), decoderAddr, sigRegistryAddr + ) → computes contentHash = keccak256(batchData) → emits CalldataBatchRegistered( - contentHash, user, decoderAddr, sigRegistryAddr + contentHash, user, contentTag, decoderAddr, sigRegistryAddr ) Client verification: 1. DECODE - - See CalldataBatchRegistered → get calldata from tx, decoderAddr, sigRegistryAddr + - See CalldataBatchRegistered → get calldata from tx, contentTag, decoderAddr, sigRegistryAddr - (messages, signatureData) = decoder.decode(batchData) → 1 message @@ -588,16 +626,17 @@ Transaction: 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) + → emits BlobSegmentDeclared(vHash, aggregator, 2000, 4096, contentTag) + → emits BlobBatchRegistered(vHash, aggregator, contentTag, decoderAddr, sigRegistryAddr) Events: - - BlobSegmentDeclared [0, 2000) "optimism.bedrock" (standalone BSS) - - BlobSegmentDeclared [2000, 4096) "social-blobs.v4" (BAM contract) - - BlobBatchRegistered (decoderAddr, sigRegistryAddr) (BAM contract) + - BlobSegmentDeclared [0, 2000) "optimism.bedrock" (standalone BSS) + - BlobSegmentDeclared [2000, 4096) "social-blobs.v4" (BAM contract) + - BlobBatchRegistered (contentTag="social-blobs.v4", decoderAddr, …) (BAM contract) Client verification: - 1. See BlobBatchRegistered → know FE range from BlobSegmentDeclared on same contract + 1. Filter BlobBatchRegistered by contentTag="social-blobs.v4" → discover the batch directly; + FE range is read from BlobSegmentDeclared on the same contract and versionedHash 2. Fetch blob, read FE [2000, 4096) 3. (messages, signatureData) = decoder.decode(segmentData) 4. Compute messageHash and signedHash for each message (standardized) @@ -643,6 +682,61 @@ BAM contracts do not require a singleton deployment. Each deployment emits `Blob 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. +### `contentTag` uniformity across blob and calldata paths + +ERC-8179 positions `contentTag` as the first-class indexer filter key: every compliant indexer is +expected to subscribe to `BlobSegmentDeclared` by `contentTag`. Earlier drafts of this ERC extended +BSS with decoder and signature-registry pointers on `registerBlobBatch` but did not carry +`contentTag` into the BAM core events, and did not accept `contentTag` on `registerCalldataBatch` +at all. Two symptoms followed. + +First, the "every BAM batch for protocol X" query was not a single filter against BAM events. A +consumer had to filter `BlobSegmentDeclared` by `contentTag`, then pair each hit with a +`BlobBatchRegistered` emitted in the same transaction against the same versioned hash — and on +shared blobs with multiple declared segments that pairing required log-index bookkeeping to stay +unambiguous. Second, calldata batches carried no protocol identifier at all, so +`CalldataBatchRegistered` was filterable only by `decoder` or `submitter`, and the "anyone can +read and filter" property did not hold uniformly across the two registration paths. + +Adding `contentTag` to both BAM core events — and to `registerCalldataBatch` — removes both +asymmetries. A single `eth_getLogs` call on either event recovers every matching registration +with its decoder and signature registry in one shot, no joins required. The calldata path gains +protocol-identity filtering at parity with the blob path. And the ERC's "contents as flexible +payload" design keeps `contentTag` as its natural per-submission schema key — usable on both +paths, not just the blob path. + +The separation of concerns the ERC already uses is preserved: + +- **`decoder`** — batch envelope parser. Relatively stable; v1 decoders use simple encodings + (ABI, RLP, SSZ) and later versions swap in compression. Convergence on a small stable set is + expected. +- **`contentTag`** — protocol / contents-schema identifier. Per-submission, higher-churn, the + primary filter key. + +#### Why `contentTag` is indexed and `decoder` is not + +Solidity caps non-anonymous events at three indexed topics. With `contentTag` promoted into the +indexed set, one existing indexed topic must move to the unindexed event data; the choice is +between `decoder` and `submitter`. + +Promoting `contentTag` into the indexed set at `decoder`'s expense reflects expected query +patterns: filter-by-`contentTag` ("every batch for protocol X") is the standard indexer query +under ERC-8179's design, while filter-by-`decoder` becomes less useful as decoders converge on a +small stable set. `submitter` remains indexed so attribution-by-submitter (the tuple +`(chainId, contentTag, submitter)` that ERC-8179 recommends for BSS indexers, and this ERC +recommends for BAM indexers) stays cheap to query. + +A minority position — keep `decoder` indexed and leave `contentTag` unindexed — preserves the +earlier layout but forces data-level filtering for the query that ERC-8179 positions as +first-class, and is rejected. + +A second alternative — declare the events `anonymous` and use four indexed topics — preserves +`decoder` indexing without demoting any other topic but sacrifices topic-0 dispatch on the event +signature. `eth_getLogs` callers would have to filter on the unkeyed event payload to disambiguate +the two BAM events from each other and from any other anonymous event sharing the same indexed +topics, which loses more discoverability than the extra indexed slot recovers, and is also +rejected. + ### Trust separation: decoder vs registry The original design bundled decoding and verification in a single "schema" contract. If a client @@ -787,8 +881,13 @@ path (`registerCalldataBatch`) works on any EVM chain. | `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)` | +| `registerCalldataBatch(data, tag, decoder, sigReg)` | 1,000 bytes of batch data | Emits `CalldataBatchRegistered` with keccak256 hash and `contentTag` indexed | +| `registerCalldataBatch(data, tag, address(0), sigReg)` | No decoder | Emits `CalldataBatchRegistered` with `decoder=address(0)` | +| `registerCalldataBatch(data, bytes32(0), decoder, sigReg)` | Null `contentTag` | Accepts; emits `contentTag=bytes32(0)` verbatim (NOT RECOMMENDED at app layer) | +| `registerBlobBatch(0, 0, 4096, bytes32(0), decoder, sigReg)` | Null `contentTag`, full blob | Accepts; emits `contentTag=bytes32(0)` verbatim on both events (NOT RECOMMENDED at app layer) | +| `registerBlobBatch(0, 0, 4096, tag, decoder, sigReg)` | Non-null tag, full blob | `contentTag` topic on `BlobSegmentDeclared` equals `contentTag` topic on `BlobBatchRegistered` | +| `eth_getLogs` on `BlobBatchRegistered` filtered by `contentTag=tag` | Two batches registered with different tags | Returns only the matching batch; filtering by an unused tag returns zero logs | +| `eth_getLogs` on `CalldataBatchRegistered` filtered by `contentTag=tag` | Two batches registered with different tags | Returns only the matching batch; filtering by an unused tag returns zero logs | ### Decoder @@ -858,11 +957,19 @@ tracked as a separate task. Deployed on Sepolia: -| Contract | Address | -| --------------- | -------------------------------------------- | -| SocialBlobsCore | `0xAdd498490f0Ffc1ba15af01D6Bf6374518fE0969` | -| BLSRegistry | `0x2146758C8f24e9A0aFf98dF3Da54eef9f53BCFbf` | -| BLSExposer | `0x0136454b435fE6cCa5F7b8A6a8cFB5B549afB717` | +| Contract | Address | +| ------------------------------ | -------------------------------------------- | +| BlobAuthenticatedMessagingCore | `0x9C4b230066a6808D83F5FBa0c040E0Df2Fcc7314` | +| SocialBlobsCore (legacy) | `0x11a825a0774d0471292eab4706743bffcdd5d137` | +| BLSRegistry | `0x15866bf5a8724f2aa9fe75e262d8f00ba2818e25` | +| BLSExposer | `0x443029b4b96fbf2d8feba77d828a394d19615a48` | + +`BlobAuthenticatedMessagingCore` is the reference implementation of the amended +ERC and emits the indexed-`contentTag` event layout described in +§Core Registration Interface. The earlier `SocialBlobsCore` deployment remains +for pre-amendment log inspection and emits the previous layout in which +`decoder` (not `contentTag`) occupied the third indexed topic; new +registrations target the BAM core address above. ### Minimal Core Implementation @@ -906,20 +1013,21 @@ contract BlobAuthenticatedMessagingCore is IERC_BAM_Core { versionedHash = declareBlobSegment(blobIndex, startFE, endFE, contentTag); emit BlobBatchRegistered( - versionedHash, msg.sender, decoder, signatureRegistry + versionedHash, msg.sender, contentTag, decoder, signatureRegistry ); } /// @inheritdoc IERC_BAM_Core function registerCalldataBatch( bytes calldata batchData, + bytes32 contentTag, address decoder, address signatureRegistry ) external returns (bytes32 contentHash) { contentHash = keccak256(batchData); emit CalldataBatchRegistered( - contentHash, msg.sender, decoder, signatureRegistry + contentHash, msg.sender, contentTag, decoder, signatureRegistry ); } } @@ -933,6 +1041,44 @@ Segment overlap — two declarations claiming overlapping field element ranges i not prevented on-chain. Clients must detect overlap by cross-referencing `BlobSegmentDeclared` events sharing the same versioned hash. +### `contentTag` tag spoofing + +`contentTag` is a caller-chosen label, not an ownership primitive. Any EOA or contract with gas +can emit a `BlobBatchRegistered` or `CalldataBatchRegistered` with any `contentTag` value — +including one associated with a well-known protocol. This mirrors ERC-8179's treatment of +`contentTag` on `BlobSegmentDeclared`. + +Indexers that treat `contentTag` as a protocol-identity claim MUST attribute each registration by +the tuple `(chainId, contentTag, submitter)` — not by `contentTag` alone — and apply whatever +submitter-level trust policy their protocol requires (e.g., an explicit allowlist of known +aggregators for that tag, or cross-checks against exposure events that prove message-level +authorship via the signature registry). This guidance applies equally to `BlobBatchRegistered` and +`CalldataBatchRegistered`; the calldata path inherits the full BSS trust model, not a weaker one. + +### `contentTag` null-tag discouragement + +A `contentTag` value of `bytes32(0)` is accepted at the contract layer for both +`registerBlobBatch` and `registerCalldataBatch` (and correspondingly for the inherited +`declareBlobSegment`). Implementations MUST NOT reject it. At the application layer, however, +`bytes32(0)` is NOT RECOMMENDED: naive consumer code is prone to treating it as an "unset tag" +sentinel, and a null-tag registration collides with that assumption. Protocols SHOULD pick a +non-null `contentTag` — typically `keccak256(".v")` — and document it alongside +their decoder and signature registry. This matches the null-tag guidance in ERC-8179. + +### `contentTag` griefing economics on the calldata path + +A griefer can spam `CalldataBatchRegistered` events with a popular `contentTag`, inflating the +log volume an indexer for that tag must sift through. The same attack exists today on the blob +path (`BlobBatchRegistered` plus `BlobSegmentDeclared`) and has always been self-limiting by +blob-gas cost: ~21,000 intrinsic gas plus the per-blob gas market. The calldata path is cheaper +per batch than the blob path — calldata gas proportional to payload size, with no blob-gas +floor — so the same griefing attack is sharper in degree on calldata than on blob registrations. +The risk is not new in kind; consumers that treated `BlobSegmentDeclared` spam as acceptable +under ERC-8179 economics SHOULD expect somewhat higher volumes on `CalldataBatchRegistered`. +Attribution via `(chainId, contentTag, submitter)` (above) is the mitigation: submitter +allowlists, reputation tracking, or exposure-event cross-checks let indexers drop non-canonical +submissions without rejecting the tag itself. + ### Batch registration spam Registering a blob batch requires a type-3 transaction with at least one blob (~21,000 intrinsic gas