Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ out/
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "lib/linea-monorepo"]
path = lib/linea-monorepo
url = https://github.com/consensys/linea-monorepo
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,43 @@
Ethereum reference implementation for [ERC-7888: Crosschain Broadcaster](https://eips.ethereum.org/EIPS/eip-7888). The standard defines a storage-proof based way to publish 32-byte messages on one chain and verify their existence on any other chain that shares a common ancestor.

- **Broadcast**: `Broadcaster` stores `block.timestamp` in slot `keccak(message, publisher)`, emits `MessageBroadcast`, and prevents duplicates per publisher.
- **Prove**: `Receiver` walks a user-specified route of `BlockHashProver` contracts to recover a finalized target block hash, proves a storage slot on a remote `Broadcaster`, checks the slot is non-zero and matches the expected `(message, publisher)` slot, then returns the timestamp.
- **Upgrade safely**: `BlockHashProverPointer` holds the latest prover implementation address and code hash in a fixed slot (`BLOCK_HASH_PROVER_POINTER_SLOT`), enforcing monotonic `version()` upgrades so routes stay stable while provers evolve.
- **Reusable proving code**: `BlockHashProver` copies can be deployed on any chain; the pointer-stored code hash guarantees the copy matches the canonical implementation.
- **Prove**: `Receiver` walks a user-specified route of `StateProver` contracts to recover a finalized target block hash, proves a storage slot on a remote `Broadcaster`, checks the slot is non-zero and matches the expected `(message, publisher)` slot, then returns the timestamp.
- **Upgrade safely**: `StateProverPointer` holds the latest prover implementation address and code hash in a fixed slot (`STATE_PROVER_POINTER_SLOT`), enforcing monotonic `version()` upgrades so routes stay stable while provers evolve.
- **Reusable proving code**: `StateProver` copies can be deployed on any chain; the pointer-stored code hash guarantees the copy matches the canonical implementation.

## Contracts
- `src/contracts/Broadcaster.sol`: Minimal broadcaster with deduplication and timestamp storage.
- `src/contracts/Receiver.sol`: Verifies broadcast messages from remote chains using a route of block-hash provers and a final storage proof; can cache prover copies.
- `src/contracts/BlockHashProverPointer.sol`: Ownable pointer storing the current prover implementation address and code hash with version monotonicity checks.
- `src/contracts/StateProverPointer.sol`: Ownable pointer storing the current prover implementation address and code hash with version monotonicity checks.
- `src/contracts/libraries/ProverUtils.sol`: Shared helpers for verifying block headers and MPT proofs (state root, account data, storage slot).
- Interfaces: `IBroadcaster`, `IReceiver`, `IBlockHashProver`, `IBlockHashProverPointer`.
- Interfaces: `IBroadcaster`, `IReceiver`, `IStateProver`, `IStateProverPointer`.

## Key concepts
- **Broadcaster**: Singleton per chain that timestamps 32-byte messages in deterministic slots and emits `MessageBroadcast`.
- **Receiver**: Trustlessly reads a remote `Broadcaster` slot by following a prover route, checking slot correctness, and returning `(broadcasterId, timestamp)`.
- **BlockHashProver**: Chain-specific verifier that proves a target block hash from a home chain state root and verifies arbitrary storage for that block.
- **BlockHashProverPointer**: Stable address that stores the prover implementation address and code hash in `BLOCK_HASH_PROVER_POINTER_SLOT`, enforcing increasing `version()`.
- **BlockHashProverCopy**: Locally deployed prover contract whose `codehash` matches the pointer; used by `Receiver` when proving multi-hop routes.
- **StateProver**: Chain-specific verifier that proves a target block hash from a home chain state root and verifies arbitrary storage for that block.
- **StateProverPointer**: Stable address that stores the prover implementation address and code hash in `STATE_PROVER_POINTER_SLOT`, enforcing increasing `version()`.
- **StateProverCopy**: Locally deployed prover contract whose `codehash` matches the pointer; used by `Receiver` when proving multi-hop routes.
- **Route**: Ordered addresses of prover pointers from the destination back to the origin chain; hashed cumulatively in `Receiver` to produce unique IDs.

## Two-hop proof flow (L2 → L1 → L2)
1) Publisher broadcasts on L2-A → `Broadcaster` stores timestamp at `keccak(message, publisher)`.
2) On L2-B, caller gives `Receiver.verifyBroadcastMessage`:
- Route: `[L2-A→L1 pointer, L1→L2-B pointer]`
- `bhpInputs[0]`: proof for L2-A block hash committed to L1
- `bhpInputs[1]`: proof for that L1 block hash committed to L2-B
- `scpInputs[0]`: proof for L2-A block hash committed to L1
- `scpInputs[1]`: proof for that L1 block hash committed to L2-B
- `storageProof`: proof for the `(message, publisher)` slot on L2-A at the proven block hash
3) `Receiver` uses a local `BlockHashProverCopy` for hop 2 (code hash must match pointer), verifies each hop, checks the slot matches `keccak(message, publisher)`, and returns `(broadcasterId, timestamp)`.
3) `Receiver` uses a local `StateProverCopy` for hop 2 (code hash must match pointer), verifies each hop, checks the slot matches `keccak(message, publisher)`, and returns `(broadcasterId, timestamp)`.
4) Subscriber contracts compare `broadcasterId` against their allowlist and mark messages as consumed.

## How the protocol fits together
1) A publisher calls `Broadcaster.broadcastMessage(message)` on Chain A. The `(message, publisher)` slot now holds the timestamp.
2) To trustlessly read that message on Chain C, a caller provides `Receiver.verifyBroadcastMessage` with:
- `route`: addresses of the `BlockHashProverPointer` hop-by-hop path (e.g., child→L1 pointer, L1→dest pointer).
- `bhpInputs`: prover-specific inputs for each hop (built off-chain with the TS helpers).
- `route`: addresses of the `StateProverPointer` hop-by-hop path (e.g., child→L1 pointer, L1→dest pointer).
- `scpInputs`: prover-specific inputs for each hop (built off-chain with the TS helpers).
- `storageProof`: a storage proof for the `Broadcaster` slot on the source chain at the proven block hash.
3) `Receiver` accumulates the route to derive unique IDs, ensures the proven slot matches `keccak(message, publisher)`, and returns `(broadcasterId, timestamp)`.
4) Before verifying, callers can seed `Receiver.updateBlockHashProverCopy` with a local prover copy whose code hash matches the pointer slot and whose `version()` increases.
4) Before verifying, callers can seed `Receiver.updateStateProverCopy` with a local prover copy whose code hash matches the pointer slot and whose `version()` increases.

## Repository layout
- `src/contracts/` – Solidity contracts and interfaces.
Expand All @@ -56,11 +56,11 @@ Ethereum reference implementation for [ERC-7888: Crosschain Broadcaster](https:/

## Using the contracts (high level)
- Deploy a `Broadcaster` on each chain where messages originate.
- Deploy a `BlockHashProverPointer` per chain pair direction; point it to the canonical `BlockHashProver` implementation (must expose `version()` and stable code hash).
- On destination chains, deploy `Receiver` and register local prover copies via `updateBlockHashProverCopy` once the pointer’s code hash is provably available.
- Deploy a `StateProverPointer` per chain pair direction; point it to the canonical `StateProver` implementation (must expose `version()` and stable code hash).
- On destination chains, deploy `Receiver` and register local prover copies via `updateStateProverCopy` once the pointer’s code hash is provably available.
- Off-chain, use the TS helpers (or your own tooling) to:
1) Find a route (e.g., L2→L1→L2),
2) Build `bhpInputs` per hop plus the final `storageProof`,
2) Build `scpInputs` per hop plus the final `storageProof`,
3) Call `Receiver.verifyBroadcastMessage` with `(message, publisher)`; use the returned `broadcasterId` to authorize the source broadcaster.

## Links
Expand Down
299 changes: 299 additions & 0 deletions docs/ERC7888.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# ERC-7888: Crosschain Broadcaster

**Reference:** [EIP-7888 Specification](https://eips.ethereum.org/EIPS/eip-7888)

## Introduction

ERC-7888 defines a protocol for trustless cross-chain message verification using cryptographic storage proofs. The protocol enables any chain to verify messages broadcast on any other chain that shares a common ancestor, creating a foundation for cross-chain interoperability without trusted intermediaries.

### Core Components

| Component | Description |
|-----------|-------------|
| **Broadcaster** | Stores messages in deterministic storage slots, one per (message, publisher) pair |
| **Receiver** | Verifies messages from remote chains using storage proofs |
| **StateProver** | Chain-specific logic for verifying state commitments and storage slots |
| **StateProverPointer** | Upgradeable pointer to the latest StateProver version |

Each chain deploys singleton `Broadcaster` and `Receiver` contracts, enabling permissionless message broadcasting and verification across the ecosystem.

### State Commitments

A **state commitment** is a `bytes32` hash that commits to a chain's state at a particular point in time. The protocol supports different types of state commitments depending on what the rollup commits to its parent chain:

- **Block hash** (recommended): A hash of the block header
- **State root**: The root of the state tree (e.g., Merkle-Patricia Trie)
- **Batch hash**: A hash of a batch of blocks (for rollups that commit batches)

---

## Broadcasting Messages

Publishers broadcast messages by calling `broadcastMessage()` on the Broadcaster contract:

```solidity
interface IBroadcaster {
/// @notice Emitted when a message is broadcast.
event MessageBroadcast(bytes32 indexed message, address indexed publisher);

/// @notice Broadcasts a message. Callers are called "publishers".
/// @dev MUST revert if the publisher has already broadcast the message.
/// MUST emit MessageBroadcast.
/// MUST store block.timestamp in slot keccak(message, msg.sender).
function broadcastMessage(bytes32 message) external;
}
```

### Storage Layout

The Broadcaster stores `block.timestamp` in storage slot `keccak256(abi.encode(message, publisher))`, creating a unique slot per (message, publisher) pair.

**Key constraints:**
- Each publisher can only broadcast a specific message **once**
- Applications requiring multiple broadcasts of the same logical message must implement nonce logic at the application layer

---

## Message Verification

### Routes

A **route** is a relative path from a Receiver on a local chain to a remote chain. Routes are constructed from StateProverPointer addresses, where each pointer lives on its home chain and references a StateProver that can prove the next chain's state commitment.

**Route validity rules:**
- Home chain of `route[0]` must equal the local chain
- Target chain of `route[i]` must equal home chain of `route[i+1]`

### Remote Account Identifiers

Accounts on remote chains are identified by accumulating the route addresses plus the remote address:

```solidity
function accumulator(address[] memory elems) pure returns (bytes32 acc) {
for (uint256 i = 0; i < elems.length; i++) {
acc = keccak256(abi.encode(acc, elems[i]));
}
}
```

IDs depend on the route and are therefore always **relative** to a local chain. The same account on a given chain will have different IDs depending on the route taken.

### Verification Process: `verifyBroadcastMessage`

```solidity
interface IReceiver {
struct RemoteReadArgs {
address[] route; // StateProverPointer addresses along the route
bytes[] scpInputs; // Inputs for each StateProver
bytes proof; // Final storage proof for the message slot
}

function verifyBroadcastMessage(
RemoteReadArgs calldata broadcasterReadArgs,
bytes32 message,
address publisher
) external view returns (bytes32 broadcasterId, uint256 timestamp);
}
```

**Three-stage verification:**

1. **Initial State Commitment Retrieval**: For the first hop, the Receiver calls the StateProverPointer at `route[0]` to get the implementation address, then calls `getTargetStateCommitment()`. Since this executes on the home chain, the prover directly accesses the target chain's state commitment.

2. **State Commitment Chain Verification**: For subsequent hops, the Receiver uses locally stored StateProverCopies to call `verifyTargetStateCommitment()` with the previous chain's state commitment and a proof. This builds a chain: `stateCommitment[0] → stateCommitment[1] → ... → stateCommitment[n]`.

3. **Storage Slot Verification**: With the target chain's state commitment established, the Receiver calls `verifyStorageSlot()` on the final StateProver, verifying:
- The slot matches `keccak256(abi.encode(message, publisher))`
- The slot value is non-zero (message was broadcast)

The slot value (`block.timestamp`) is returned as the timestamp.

---

## StateProvers

A `StateProver` implements chain-specific verification logic. Each rollup stores parent/child state commitments differently, requiring custom prover implementations.

```solidity
interface IStateProver {
/// @notice Get the state commitment of the target chain when called on the home chain.
/// @dev MUST revert if not called on the home chain.
function getTargetStateCommitment(bytes calldata input)
external view returns (bytes32 targetStateCommitment);

/// @notice Verify the state commitment of the target chain from a remote chain.
/// @dev MUST revert if called on the home chain.
function verifyTargetStateCommitment(bytes32 homeStateCommitment, bytes calldata input)
external view returns (bytes32 targetStateCommitment);

/// @notice Verify a storage slot given a target chain state commitment.
function verifyStorageSlot(bytes32 targetStateCommitment, bytes calldata input)
external view returns (address account, uint256 slot, bytes32 value);

/// @notice The version of this prover implementation.
function version() external pure returns (uint256);
}
```

### Operating Modes

| Mode | Function | Context |
|------|----------|---------|
| **Home chain** | `getTargetStateCommitment()` | Directly reads target chain's state commitment from storage |
| **Remote chain** | `verifyTargetStateCommitment()` | Verifies a proof establishing the target chain's state commitment |

### Prover Requirements

- **Code hash consistency**: StateProvers MUST ensure they have the same deployed code hash on all chains
- **Pure functions**: `verifyTargetStateCommitment()`, `verifyStorageSlot()`, and `version()` MUST be pure (with exception: MAY read `address(this).code`)
- **Chain-specific logic**: Each prover is fixed to a specific (home chain, target chain) pair

---

## StateProverPointers

Rollup storage layouts may change over time, requiring prover updates. However, routes reference provers by address, and changing addresses would break existing routes. `StateProverPointers` provide indirection to enable upgrades without breaking routes.

```solidity
// Fixed storage slot for the code hash
uint256 constant STATE_PROVER_POINTER_SLOT = uint256(keccak256("eip7888.pointer.slot")) - 1;

interface IStateProverPointer {
/// @notice Return the code hash of the latest version of the prover.
function implementationCodeHash() external view returns (bytes32);

/// @notice Return the address of the latest version of the prover on the home chain.
function implementationAddress() external view returns (address);
}
```

### Upgrade Process

1. Deploy a new `StateProver` with a higher `version()` number
2. Pointer owner calls `setImplementationAddress()` to point to the new prover
3. Pointer stores the new prover's code hash in `STATE_PROVER_POINTER_SLOT`
4. Receivers update local copies via `updateStateProverCopy()`, which verifies the code hash matches

**Upgrade constraints:**
- The new StateProver MUST have the same home and target chains
- The new StateProver MUST have a higher `version()` than the previous one

### Security Considerations for Pointer Ownership

The StateProverPointer owner can DoS or forge messages. Therefore:

| Target Chain | Expected Owner |
|--------------|----------------|
| Parent chain | Home chain owner |
| Child chain | Target chain owner |

---

## StateProverCopies

Receivers cannot call contracts on remote chains directly. To verify proofs from remote chains, Receivers maintain local copies of StateProvers with matching code.

### Copy Registration

```solidity
function updateStateProverCopy(
RemoteReadArgs calldata scpPointerReadArgs,
IStateProver scpCopy
) external returns (bytes32 scpPointerId);
```

The Receiver verifies:
1. The proof reads from `STATE_PROVER_POINTER_SLOT`
2. The copy's code hash matches the Pointer's stored code hash
3. The copy's version is higher than any existing copy (if updating)

Copies are stored keyed by `scpPointerId`, calculated by accumulating hashes of addresses in the route to the Pointer.

---

## Complete Interface Reference

### IBroadcaster

```solidity
interface IBroadcaster {
event MessageBroadcast(bytes32 indexed message, address indexed publisher);

function broadcastMessage(bytes32 message) external;
}
```

### IReceiver

```solidity
interface IReceiver {
struct RemoteReadArgs {
address[] route;
bytes[] scpInputs;
bytes proof;
}

function verifyBroadcastMessage(
RemoteReadArgs calldata broadcasterReadArgs,
bytes32 message,
address publisher
) external view returns (bytes32 broadcasterId, uint256 timestamp);

function updateStateProverCopy(
RemoteReadArgs calldata scpPointerReadArgs,
IStateProver scpCopy
) external returns (bytes32 scpPointerId);

function stateProverCopy(bytes32 scpPointerId)
external view returns (IStateProver scpCopy);
}
```

### IStateProver

```solidity
interface IStateProver {
function verifyTargetStateCommitment(bytes32 homeStateCommitment, bytes calldata input)
external view returns (bytes32 targetStateCommitment);

function getTargetStateCommitment(bytes calldata input)
external view returns (bytes32 targetStateCommitment);

function verifyStorageSlot(bytes32 targetStateCommitment, bytes calldata input)
external view returns (address account, uint256 slot, bytes32 value);

function version() external pure returns (uint256);
}
```

### IStateProverPointer

```solidity
interface IStateProverPointer {
function implementationCodeHash() external view returns (bytes32);
function implementationAddress() external view returns (address);
}
```

---

## Security Considerations

### Chain Upgrades

If a chain upgrades such that a StateProver's verification functions might return data besides a finalized target state commitment, invalid messages could be read. Either:
- The StateProver should detect such changes
- The chain owner responsible for storage layout changes should also own the StateProverPointer

### Message Guarantees

This protocol ensures messages **CAN** be read, not that they **WILL** be read. It is the Receiver caller's responsibility to choose which messages to read.

Since the protocol only uses finalized blocks, messages may take time to propagate. Finalization occurs sequentially in the route, so total propagation time equals the sum of finalization times at each step.

---

## Links

- **EIP Specification**: [EIP-7888](https://eips.ethereum.org/EIPS/eip-7888)
- **Discussion**: [Ethereum Magicians Forum](https://ethereum-magicians.org/t/new-erc-cross-chain-broadcaster/22927)
Loading