This ERC defines a protocol for cross-rollup messaging using storage proofs. Users can broadcast messages on a source chain, and those messages can be verified on any other chain that shares a common ancestor with the source chain.
Each chain deploys a singleton Receiver and Broadcaster contract. Broadcasters store messages; Receivers verify the Broadcasters’ storage on remote chains. To do this, a Receiver first verifies a chain of storage proofs to recover a remote block hash, then verifies the Broadcaster’s storage at that block.
Critically, the logic for verifying storage proofs is not hardcoded in the Receiver. Instead, it delegates this to a user specified list of BlockHashProver contracts. Each BlockHashProver defines how to verify a storage proof for a specific home chain to recover the block hash of a specific target chain. Because the storage layouts of rollup contracts can change over time, the storage proof verification process itself must also be upgradeable—hence the BlockHashProvers are upgradeable. This flexible, upgradeable proof verification model is the core contribution of this standard.
The Ethereum ecosystem is experiencing a rapid growth in the number of rollup chains. As the number of chains grows, the experience becomes more fragmented for users, creating a need for trustless "interop" between rollup chains. These rollup chains, hosted on different rollup stacks, have heterogeneous properties, and as yet there does not exist a simple, trustless, unified mechanism for sending messages between these diverse chains.
Many classes of applications could benefit from a unified system for broadcasting messages across chains. Some examples include:
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Chains must satisfy the following conditions to be compatible with the system:
The Broadcaster is responsible for storing messages in state to be read by Receivers on other chains. Callers of the Broadcaster are known as Publishers. The Broadcaster stores only 32 byte messages.
The Broadcaster does not accept duplicate messages from the same publisher.
BlockHashProvers prove a unidirectional link between two chains that have direct access to each other's finalized blocks. The chains in this link are called the home chain and the target chain. BlockHashProvers are responsible for verifying storage proofs to prove the existence of finalized target block hashes in the state of the home chain.
Since the BlockHashProvers are unidirectional, each chain needs to have two:
BlockHashProvers MUST ensure that they will have the same deployed code hash on all chains.
BlockHashProvers can be used to get or verify target block hashes, however since their verification logic is immutable, changes to the structure of the home or target chain can break the logic in these Provers. A BlockHashProverPointer is a Pointer to a BlockHashProver which can be updated if proving logic needs to change.
BlockHashProverPointers are used to reference BlockHashProvers as opposed to referencing Provers directly. To that end, wherever a BlockHashProver is deployed a BlockHashProverPointer needs to be deployed to reference it.
BlockHashProverPointers allow a permissioned party to update the Prover reference within the Pointer. Choosing which party should have the permission to update the Prover reference should be carefully considered. The general rule is that if an update to the target or home chain could break the logic in the current Prover, then the party, or mechanism, able to make that update should also be given permission to update the Prover. See Security Considerations for more information on BlockHashProverPointer ownership and updates.
When updating a BlockHashProverPointer to point to a new BlockHashProver implementation:
BlockHashProverPointers MUST store the code hash of the BlockHashProver implementation in slot BLOCK_HASH_PROVER_POINTER_SLOT.
A route is a relative path from a Receiver on a local chain to a remote chain. It is constructed of many single degree links dictated by BlockHashProverPointers. Receivers use the BlockHashProvers that the Pointers reference to verify a series of proofs to obtain the remote chain's block hash. A route defined by the list of Pointer addresses on their home chains.
A valid route MUST obey the following:
route[0] Pointer must equal the local chainroute[i] Pointer must equal home chain of the route[i+1] PointerAccounts on remote chains are identified by the route taken from the local chain plus the address on the remote chain. The Pointer addresses used in the route, along with the remote address, are cumulatively keccak256 hashed together to form a Remote Account ID.
In this way any address on a remote chain, including Pointers and Broadcasters, can be uniquely identified relative to the local chain by their Remote Account ID.
ID's depend on a route and are therefore always relative to a local chain. In other words, the same account on a given chain will have different ID's depending on the route from the local chain.
The Remote Account ID is defined as accumulator([...route, remoteAddress])
In Figure 4:
0x3 is accumulator([0xA, 0xB, 0xC, 0x3])0xC is accumulator([0xA, 0xB, 0xC]).BlockHashProverCopies are exact copies of BlockHashProvers deployed on non-home chains. When a BlockHashProver code hash is de-referenced from a Pointer, a copy of the BlockHashProver may be used to execute its logic. Since the Pointer references the prover by code hash, a local copy of the Prover can be deployed and used to execute specific proving logic. The Receiver caches a map of mapping(bytes32 blockHashProverPointerId => IBlockHashProver blockHashProverCopy) to keep track of BlockHashProverCopies.
The Receiver is responsible for verifying 32 byte messages deposited in Broadcasters on other chains. The caller provides the Receiver with a route to the remote account and proof to verify the route.
The calls in Figure 6 perform the following operations:
IReceiver::verifyBroadcastMessage, passing route [0xA, 0xB, 0xC], proof data, message, publisher.IBlockHashProverPointer(0xA)::implementationAddress to get the address of BlockHashProver L->MIBlockHashProver(Prover L->M)::getTargetBlockHash, passing input given by Subscriber to get a block hash of chain M.IBlockHashProver(Prover Copy M->P)::verifyTargetBlockHash, passing chain M's block hash and proof data by Subscriber to get a block hash of chain P.IBlockHashProver(Prover Copy P->R)::verifyTargetBlockHash, passing chain P's block hash and proof data by Subscriber to get a block hash of chain R.IBlockHashProver(Prover Copy P->R)::verifyStorageSlot, passing input given by Subscriber to get a storage slot from the Broadcaster. The Receiver returns the Broadcaster's Remote Account ID and the message's timestamp to Subscriber.A contract on any given chain cannot dictate which other chains can and cannot inspect its state. Contracts are naturally broadcasting their state to anything capable of reading it. Targeted messaging applications can always be built on top of a broadcast messaging system.
See Reference Implementation for an example of a unicast application.
Message reading uses storage proofs. An alternative to this would be to pass messages (perhaps batched) via the canonical bridges of the chains. However storage proofs have some advantages over this method:
To allow publishers to send the same message multiple times, some kind of nonce system would need to exist in this ERC. Since nonces can be implemented at the Publisher / Subscriber layer, and not all Publishers / Subscribers require this feature, it is left out of this ERC.
Here we compare the cost of using storage proofs vs sending messages via the canonical bridge, where the parent chain is Ethereum. Here, we will only consider the cost of the L1 gas as we assume it to dominate the L2 gas costs.
Each step along the route requires 1 storage proof. These proofs can be estimated at roughly 6.5k bytes. These proofs will likely be submitted on an L2/L3 and therefore be included in blobs on the L1, which have a fluctuating blob gas price. Since rollups can dynamically switch between calldata and blobs, we can work out a maximum amount of normal L1 gas that could be using the standard cost of calldata as an upper bound. Post Pectra, the upper bound for non-zero-byte calldata is 40 gas per byte, which for 6.5k bytes equates to 260,000 L1 gas.
We want to compare this to sending a single message via a canonical rollup bridge, which is either a parent->child or child->parent message. This estimate is dependent on specific implementations of the bridge for different rollup frameworks, but we estimate it to be around 150,000 gas.
This puts the upper bound of the storage proof to be around 2x that of the canonical bridge, but in practice this upper bound is rarely reached. On top of that, the Receiver can implement a caching policy allowing many messages to share the same storage proofs.
This ERC does not currently describe how the Receiver can cache the results of storage proofs to improve efficiency. In brief, once a storage proof is executed it never needs to be executed again, and instead the result can be stored by the Receiver. This allows messages that share the same, or partially the same, route to share previously executed storage proofs and instead lookup the result. As an example we can consider the route between two L2s:
Chains are often identified by chain ID's. Chain ID's are set by the chain owner so they are not guaranteed to be unique. Using the addresses of the Pointers is guaranteed to be unique as it provides a way to unwrap the nested block hashes embedded in the state roots. A storage slot on a remote chain can be identified by many different remote account ID's, but one remote account ID cannot identify more than one storage slot.
Each rollup implements unique logic for managing and storing block hashes. To accommodate this diversity, BlockHashProvers implement chain-specific procedures. This flexibility allows integration with each rollup's distinct architecture.
The BlockHashProver handles the final step of verifying a storage slot given a target block hash to accommodate rollups with differing state trie formats.
Routes reference BlockHashProvers through Pointers rather than directly. This indirection is crucial because:
Since BlockHashProverPointers reference BlockHashProvers via their code hash, a copy of the BlockHashProver can be deployed anywhere and reliably understood to contain the same code as that referenced by the Pointer. This allows the Receiver to locally use the code of a BlockHashProver whose home chain is a remote chain.
The following is an example of a one-way crosschain token migrator. The burn side of the migrator is a publisher which sends burn messages through a Broadcaster. The mint side subscribes to these burn messages through a Receiver on another chain.
If a chain upgrades such that a BlockHashProver's verifyTargetBlockHash or getTargetBlockHash functions might return data besides a finalized target block hash, then invalid messages could be read by a Receiver. For instance, if a chain stores its block hashes on the parent chain in a specific mapping, and that storage location is later repurposed, then an old BlockHashProver might be able to pass along an invalid block hash. It is therefore important that either:
A malicious BlockHashProverPointer owner can DoS or forge messages. However, so can the chain owner responsible for setting the location of historical parent/child block hashes. Therefore it is expected that this chain owner be the same as the owner of the BlockHashProverPointer so as not to introduce additional risks.
If an owner neglects their responsibility to update the Pointer with new BlockHashProver implementations when necessary, messages could fail to reach their destinations.
If an owner maliciously updates a Pointer to point to a BlockHashProver that produces fraudulent results, messages can be forged.
If there is confidence that a chain along the route connecting them will not upgrade to break a BlockHashProver, an unowned BlockHashProverPointer can be deployed in the absence of a properly owned one.
This ERC describes a protocol for ensuring that messages from remote chains CAN be read, but not that they WILL be read. It is the responsibility of the Receiver caller to choose which messages they wish to read.
Since the ERC only uses finalized blocks, messages may take a long time to propagate between chains. Finalisation occurs sequentially in the route, therefore time to read a message is the sum of the finalisation of each of the block hashes at each step in the route.
Copyright and related rights waived via CC0.