EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8281: Observation Commitment Protocol

A minimal proof envelope and verification invariant for independently confirming that a specific byte sequence was committed on-chain.

Authors
Created2026-04-05
Discussion Linkhttps://ethereum-magicians.org/t/erc-8281-observation-commitment-protocol-ocp/28399
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1788PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationContract InterfaceHash Function RegistryProof Envelope SchemaExtraction RuleVerification InvariantOut of ScopeRationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

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

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

IdentifierAlgorithmOutput bitsNotes
sha2-256SHA-256256RECOMMENDED default
keccak-256Keccak-256256EVM-native
blake2b-256BLAKE2b256BLAKE2b 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.

{
  "version":               "erc8281/1",
  "digest":                "<0x-prefixed lowercase hex-encoded bytes32>",
  "hash_function":         "<identifier from Hash Function Registry>",
  "chain_id":              "<EIP-155 chain ID as decimal string, no leading zeros>",
  "contract":              "<EIP-55 checksummed EVM address>",
  "tx_hash":               "<0x-prefixed lowercase hex-encoded transaction hash>",
  "block_number":          "<decimal block number, no leading zeros>",
  "receipt_log_position":  "<decimal zero-indexed position of the matching log within the receipt's log array, no leading zeros>",
  "committer":             "<EIP-55 checksummed EVM address>"
}

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 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 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:

{
  "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 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 checksum validation
  14. Constant verification — the topic-0 literal 0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497 and the ERC-165 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):

NetworkAddressNotes
Ethereum mainnet0x7186599d6Eb50905ecd34346e705E4C93871143bv2 conformant. Verified on Etherscan.
Arbitrum One0x65884e7db1E57cA2AEf0d66eFcff9c738684B02av2 conformant.
Base Sepolia0x0963Fd33DF80c94360F2DC22e5c09517AeE7ED5cv0 — 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 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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth