--- eip: 8281 title: Observation Commitment Protocol description: A minimal proof envelope and verification invariant for independently confirming that a specific byte sequence was committed on-chain. author: Damon Zwicker (@damonzwicker) discussions-to: https://ethereum-magicians.org/t/erc-8281-observation-commitment-protocol-ocp/28399 status: Draft type: Standards Track category: ERC created: 2026-04-05 requires: 55, 155, 165 --- ## Abstract This ERC defines a minimal proof envelope format, an extraction rule for retrieving committed digests from raw EVM ledger data, and a verification invariant: recompute → compare → confirm inclusion. A conforming verifier confirms that a specific byte sequence was committed on-chain at a given point in time using raw transaction receipt data obtained from an RPC endpoint or local node. No trusted SDK, gateway, or indexer is required, though verifiers are advised to cross-check multiple independent endpoints to avoid trusting any single RPC provider. This ERC is scoped to EVM-compatible chains. ## Motivation Ethereum does not define a standard procedure for a third party to confirm that a specific byte sequence was committed on-chain, independent of the system that produced the commitment. In practice, each implementation defines its own encoding of observed data, its own proof artifact structure, its own transaction reference format, and its own verification logic. Proofs produced by one system are not independently verifiable by another without custom integration. The gap is not access to data or correctness of execution. The gap is the absence of a shared verification boundary — a minimal, portable procedure that any third party can follow to confirm a commitment without trusting the originating system. This ERC defines that boundary. ## 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. ### Contract Interface ```solidity // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; interface IObservationCommitment { /// @notice Commit a digest on-chain. /// @param digest The hash of the observation bytes, produced using /// a hash function from the allowed set defined in this ERC. function record(bytes32 digest) external; /// @notice Emitted on every successful record() call. /// @param digest The committed digest (topics[1]). /// @param committer The address that called record() (topics[2]). event Recorded( bytes32 indexed digest, address indexed committer ); } ``` The canonical event signature is `Recorded(bytes32,address)`. The canonical event topic-0 is: `keccak256("Recorded(bytes32,address)")` = `0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497` Implementations SHOULD support [ERC-165](./eip-165.md) with the `IObservationCommitment` interface ID to enable on-chain discoverability. Because the interface contains a single function, the interface ID is the selector of `record(bytes32)`: `bytes4(keccak256("record(bytes32)"))` = `0xb5c645bd` [ERC-165](./eip-165.md) support enables on-chain discovery of contracts that claim support for `IObservationCommitment`. It does not by itself establish behavioral conformance; relying parties must independently decide whether to trust or validate the emitting contract. > **Note on delegatecall and proxies:** Logs emitted via delegatecall carry the proxy contract's address in the log's `address` field. The `contract` field in the proof envelope MUST match the emitting address exactly as recorded in the receipt log — not the implementation address. ### Hash Function Registry The `hash_function` field in the proof envelope MUST be drawn from the following allowed set. Identifiers follow the Multihash naming convention, lowercase, hyphen-separated. | Identifier | Algorithm | Output bits | Notes | |----------------|------------|-------------|------------------------------------------------------------------| | `sha2-256` | SHA-256 | 256 | RECOMMENDED default | | `keccak-256` | Keccak-256 | 256 | EVM-native | | `blake2b-256` | BLAKE2b | 256 | BLAKE2b with 256-bit digest parameter (NOT truncated BLAKE2b-512) | All identifiers are case-sensitive lowercase. Verifiers MUST reject envelopes declaring a `hash_function` not present in this table. All allowed hash functions natively produce 256-bit (32-byte) output, matching the `bytes32` parameter of `record()`. Truncation or padding of hash output is not permitted. In particular, `blake2b-256` denotes BLAKE2b initialized with a 256-bit digest-length parameter; this produces a different digest than truncating BLAKE2b-512 output to 32 bytes, and only the parameterized form is conformant. This registry is closed for the lifetime of this ERC. Additions to the allowed set require a successor ERC. The registry will never be extended with functions providing less than 128 bits of collision resistance. ### Proof Envelope Schema A proof envelope is a JSON object. The following fields are all REQUIRED. Additional fields not listed here MUST be ignored by verifiers. ```json { "version": "erc8281/1", "digest": "<0x-prefixed lowercase hex-encoded bytes32>", "hash_function": "", "chain_id": "", "contract": "", "tx_hash": "<0x-prefixed lowercase hex-encoded transaction hash>", "block_number": "", "receipt_log_position": "", "committer": "" } ``` **Field normalization rules:** - `digest`: MUST be 0x-prefixed, 66 characters total, lowercase hex. Comparisons MUST be performed over the decoded 32-byte value, not the string. - `chain_id`: MUST be a decimal integer string with no leading zeros. Comparisons MUST be performed over the decoded integer value. - `contract`: MUST be checksummed per [ERC-55](./eip-55.md) Verifiers MUST reject envelopes containing addresses that fail ERC-55 checksum validation. Address comparisons MUST be performed over the decoded 20-byte value. - `tx_hash`: MUST be 0x-prefixed, 66 characters total, lowercase hex. - `block_number`: MUST be a decimal integer string with no leading zeros. - `receipt_log_position`: MUST be a decimal integer string with no leading zeros. This field is the zero-indexed position of the matching log within the receipt's `logs` array. It uniquely identifies a single log entry within the receipt and is used to select exactly one log during extraction. > **Warning — this is NOT the JSON-RPC `logIndex` field.** The `logIndex` field returned by JSON-RPC on each log object is the log's position within the *block*, not within the receipt. Implementations MUST select `receipt.logs[receipt_log_position]` by array position and MUST NOT compare this field against the JSON-RPC `logIndex` value. - `committer`: MUST be checksummed per [ERC-55](./eip-55.md) Verifiers MUST reject envelopes containing addresses that fail ERC-55 checksum validation. Address comparisons MUST be performed over the decoded 20-byte value. **Optional fields:** ```json { "block_hash": "<0x-prefixed lowercase hex-encoded block hash>" } ``` `block_hash` is OPTIONAL. It MUST be 0x-prefixed, 66 characters total, lowercase hex. When present, verifiers MUST assert that it equals the `blockHash` field of the fetched transaction receipt (decoded 32-byte comparison). This provides a reorg-detection mechanism: if the transaction has migrated to a different block after a chain reorganization, the assertion fails. **Version semantics:** The version string for this ERC is `erc8281/1`, compared by exact, case-sensitive string match. Verifiers MUST reject envelopes whose `version` value they do not implement. The version increments only on breaking changes to the envelope schema or verification procedure; non-breaking clarifications do not change the version. Any future breaking revision requires a successor ERC and a new version string. ### Extraction Rule Given a proof envelope, the extraction procedure is: 1. Fetch the transaction receipt for `tx_hash` on `chain_id` via an RPC endpoint or local node. Before using the endpoint, the verifier MUST confirm that it serves the declared chain: the endpoint's `eth_chainId` MUST equal the envelope's `chain_id` (decoded integer comparison). If the chain IDs do not match, extraction fails. The receipt MUST have `status == 1` (success). A reverted transaction MUST NOT be treated as a valid commitment. 2. Select the log at array position `receipt_log_position` within the receipt's `logs` array (zero-indexed). If no log exists at that position, extraction fails. 3. Assert that the selected log's `address` field matches `contract` (decoded 20-byte comparison). 4. Assert that the selected log contains exactly three topics, and that `topics[0]` matches `keccak256("Recorded(bytes32,address)")` = `0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497`. A log with fewer or more than three topics MUST be rejected, even if `topics[0]` matches. The log's `data` field is not used by this procedure and MUST be ignored. 5. Decode `topics[1]` as `bytes32`. This is the on-chain digest. Because `digest` is an indexed parameter of fixed-size type, `topics[1]` contains the digest value verbatim — it is not hashed, unlike indexed parameters of dynamic types. 6. Decode `topics[2]` as `address` (20 bytes, right-aligned in the 32-byte topic). This is the on-chain committer. If `block_hash` is present in the envelope, additionally assert that it equals the receipt's `blockHash` field (decoded 32-byte comparison). ### Verification Invariant A conforming verifier MUST execute the following procedure. The observation bytes are an input supplied to the verifier out-of-band — they are not encoded in the envelope. Verification is performed over exactly those bytes as provided. 1. **Recompute:** Apply the `hash_function` declared in the envelope to the supplied observation bytes. Produce `H_prime`. The verifier MUST reject envelopes declaring a `hash_function` not in the allowed set defined in this ERC. 2. **Compare:** Assert `H_prime == digest` (decoded bytes comparison). 3. **Confirm inclusion:** Apply the extraction rule, including all assertions it contains. Assert `on_chain_digest == digest` (decoded bytes comparison). 4. **Confirm committer:** Assert `on_chain_committer == committer` (decoded address comparison). 5. **Confirm block:** Assert the receipt's `blockNumber == block_number` (integer comparison). If all assertions pass — including those internal to the extraction rule — the named contract emitted a `Recorded` log at the referenced block containing the digest of the supplied observation bytes and the declared committer value, independently of the system that produced the proof envelope. If the emitting contract is separately trusted or validated as conforming, the committer corresponds to the address that called `record()`. The digest is self-verifying. The committer is not. A verifier recomputes the digest from the observation bytes in their own possession — that step is independent of the emitting contract's behavior. The committer field is asserted by the contract; nothing in the verification procedure recomputes it. Receipt extraction establishes that the named contract logged this digest and committer value; whether committer equals the address that called record() is conditional on the emitting contract being separately trusted or validated as conforming. If any assertion fails, the verification MUST be rejected. Partial verification — passing some assertions but not others — MUST NOT be treated as a valid commitment. **Authenticated vs. informational fields:** The procedure above authenticates the following envelope fields: `digest`, `hash_function`, `chain_id`, `contract`, `tx_hash`, `receipt_log_position`, `committer`, `block_number`, and `block_hash` when present. The `version` field is checked for implementer support. No other semantic claims in the envelope are authenticated by this procedure. Relying parties MUST NOT treat a passing verification as an endorsement of any field or claim not listed here. ### Out of Scope This ERC does not define identity, authorship, sanitization, data availability, canonical encoding of observation content, or application-layer semantics. These are explicit non-goals. Systems building on this standard are responsible for the layers above this boundary. This ERC does not define an extraction profile for non-EVM ledgers. Implementations on non-EVM chains are outside the scope of this standard and MUST NOT claim conformance with this ERC. This ERC does not define linkage between successive commitments — supersession, amendment, or parent/successor ordering across multiple record() calls. A committed digest is a single-shot fact; sequencing and current-state interpretation are the responsibility of the layer that references it. ## Rationale The verification invariant is the smallest boundary that enables independent, system-agnostic verification of an on-chain commitment. The proof envelope bundles the committed digest and chain reference into a single portable artifact that travels independently of the infrastructure that produced it. **Why receipt_log_position instead of any-match semantics:** A single transaction may emit multiple `Recorded` events from the same contract. Any-match semantics — accepting verification if any matching log contains the digest — create ambiguity about which commitment is being verified and allow a prover to front-run a legitimate commitment with a malicious one in the same transaction. `receipt_log_position` uniquely identifies a single log entry and removes this ambiguity. **Why the field is named receipt_log_position rather than log_index:** The JSON-RPC log object exposes a `logIndex` field that is block-scoped, not receipt-scoped. A field named `log_index` invites implementers to compare against the JSON-RPC value, which only coincides with the receipt-array position for the first transaction in a block. The name `receipt_log_position` makes the receipt-scoped semantics explicit. **Why timestamp was removed from the Recorded event:** Block timestamps are set by block producers within consensus rules and are derivable from `block_number` via any RPC endpoint. Storing a timestamp in the event costs gas, creates a false-authenticity surface (the contract could emit an arbitrary value), and is redundant. Block time is the authoritative timestamp; `block_number` is the reference. **Why hash_function is constrained to a registry:** A free-form hash_function field allows a malicious prover to declare a weak hash function (e.g., MD5), present a colliding observation, and pass all verification assertions. Constraining to a named registry with a minimum security level (256-bit output, ≥128-bit collision resistance) closes this attack vector at the spec level. **Why the verifier must reject unrecognized identifiers:** The registry constraint binds committers. A separate requirement that verifiers MUST reject unrecognized identifiers ensures that both sides of the protocol enforce the registry — a committer cannot declare a weak function and find a naive verifier that accepts it. **Why the registry is closed:** A Final ERC is immutable, so "amending this ERC" is not an available extension mechanism. Making the registry explicitly closed, with additions delegated to a successor ERC, keeps the conformance target unambiguous for the lifetime of this document. **Why this ERC is EVM-scoped:** The extraction rule depends on EVM transaction receipt structure, topics, and [EIP-155](./eip-155.md) chain IDs. Extending to non-EVM chains requires a different extraction profile. An earlier draft implied chain-agnosticism that the specification did not deliver; this revision aligns them. The contract interface is minimal by design. `record()` takes a single `bytes32` digest and emits a single `Recorded` event. The contract enforces no constraints on the digest content, the committer identity, or the observation semantics. These are the responsibility of the caller. ## Backwards Compatibility No backwards compatibility issues with the Ethereum protocol. This ERC introduces no changes to the protocol layer. **Breaking changes from v0:** - The `Recorded` event signature has changed from `Recorded(bytes32,address,uint256)` to `Recorded(bytes32,address)`. The topic-0 hash has changed accordingly. Any deployment using the v0 event signature is not conformant with this revision. Implementers upgrading from v0 MUST redeploy the contract and reissue proof envelopes referencing the new contract address. - The proof envelope adds a REQUIRED `receipt_log_position` field and removes the `timestamp` field. The version string has changed from `ocp/1.0` to `erc8281/1`. v0 envelopes are not conformant with this revision. ## Test Cases Test vectors are provided in the assets directory as test-vectors.json. The suite covers: 1. Correct verification — SHA-256 observation, valid envelope, all assertions passing 2. Correct verification — Keccak-256 observation, valid envelope 3. Correct verification — BLAKE2b-256 (parameterized) observation, valid envelope; includes a negative companion vector confirming that truncated BLAKE2b-512 output does NOT verify 4. Rejection — digest mismatch (`H_prime != digest`) 5. Rejection — on-chain digest mismatch (digest not at declared `receipt_log_position`) 6. Rejection — committer mismatch 7. Rejection — `block_number` mismatch 8. Rejection — unrecognized `hash_function` identifier 9. Rejection — receipt `status == 0` (reverted transaction) 10. Rejection — `receipt_log_position` out of range 11. Rejection — contract address mismatch at `receipt_log_position` 12. Rejection — `block_hash` mismatch (optional field, when present) 13. Rejection — address failing [ERC-55](./eip-55.md) checksum validation 14. Constant verification — the topic-0 literal `0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497` and the [ERC-165](./eip-165.md) interface ID `0xb5c645bd` match values computed from the signature strings 15. Distinction vector — a receipt whose first log's JSON-RPC `logIndex` differs from its receipt-array position, confirming implementations select by array position 16. Rejection — endpoint `eth_chainId` does not match envelope `chain_id` (wrong-chain endpoint) 17. Rejection — log with matching `topics[0]` but fewer than three topics (hand-crafted assembly log) ## Reference Implementation A reference contract implementation (ObservationCommitment.sol), interface (IObservationCommitment.sol), and reference verifier (verify.mjs) are provided in the assets directory of this repository. The reference verifier is also published on npm as `ocp-verify` (version 2.0.0). Supplementary deployment addresses (not normative): | Network | Address | Notes | |---|---|---| | Ethereum mainnet | `0x7186599d6Eb50905ecd34346e705E4C93871143b` | v2 conformant. Verified on Etherscan. | | Arbitrum One | `0x65884e7db1E57cA2AEf0d66eFcff9c738684B02a` | v2 conformant. | | Base Sepolia | `0x0963Fd33DF80c94360F2DC22e5c09517AeE7ED5c` | v0 — non-conformant with this revision. | ## Security Considerations **Hash function allowlist:** Verifiers MUST reject envelopes declaring hash functions not present in the registry defined in this ERC. A prover who can choose the hash function can choose a weak one and present a colliding observation. The allowlist closes this at the verifier level regardless of what a committer declares. **Digest collision resistance:** All hash functions in the allowed set provide at least 128 bits of collision resistance, and the registry will never be extended with functions below this threshold. **Low-entropy observations:** The commitment is binding but not hiding. A digest of a predictable document (a form, a vote, a price, a small integer) is a dictionary-attack target — the public digest leaks the content to anyone who can enumerate the observation space. Committers of low-entropy observations SHOULD salt the observation before hashing and retain the salt alongside the observation for verification. The salt MUST be supplied to the verifier as part of the observation bytes. **Transaction receipt integrity:** Receipt integrity is guaranteed by the chain's consensus mechanism. A verifier MAY independently verify the receipt against a block header using standard Merkle Patricia Trie proof techniques. Verifiers that do not perform header verification are trusting the RPC provider. **RPC trust:** Querying a single `eth_getTransactionReceipt` endpoint trusts that endpoint to return honest data. Verifiers SHOULD cross-check results across multiple independent RPC providers. Verifiers requiring stronger guarantees SHOULD verify the receipt against a block header obtained from a light client or other independent source. **Finality and reorgs:** A commitment at a given block is only as final as that block. On proof-of-stake Ethereum, blocks reach economic finality after approximately two epochs (~12.8 minutes). On L2 networks, sequencer-confirmed blocks may not yet be settled to L1. Verifiers SHOULD require finalized blocks or a confirmation depth appropriate to the chain and the value at risk. The optional `block_hash` field provides a reorg-detection mechanism — when present, a receipt that has migrated to a different block fails verification. **Contract trust:** Verification establishes that the named contract emitted a `Recorded` event with the declared digest. It does not establish that the contract is a conforming deployment of this ERC or that the committer had any particular authorization. Any contract — conforming or not — can emit an event with the canonical signature. Relying parties MUST independently decide whether to trust the contract address — for example, by checking [ERC-165](./eip-165.md) support for interface ID `0xb5c645bd`, or by consulting a known registry of conforming deployments. **Committer address does not imply authorship:** The `committer` field identifies the address that called `record()` — this relationship follows from conforming contract behavior, but is not established by receipt extraction alone (see **Contract trust** above). It does not imply that the committer authored the observation, holds any identity claim, or had any authority over the observed data. Identity binding is the responsibility of higher-layer protocols. **No revocation:** This ERC does not define a revocation mechanism. A committed digest cannot be un-committed. Systems requiring revocation MUST implement it at the application layer. **Timestamp precision:** Header validity rules require each block's timestamp to be strictly greater than its parent's. Post-merge, block timestamps are determined by slot number (12-second slots) and are accurate to the slot interval. Applications requiring precise timing SHOULD use `block_number` as the primary temporal reference and treat block timestamps as approximate to the slot interval. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).