This ERC defines a protocol for cross-rollup messaging using state commitments. 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.
A state commitment is a bytes32 hash that commits to a chain's state (e.g., block hash or state root). The protocol supports different types of state commitments depending on what the rollup commits to its parent chain. Block hashes are the recommended state commitment, but state roots or other commitments may be used, such as batch hashes (since some rollups don't commit single blocks, but batches of blocks instead).
Each chain deploys a singleton Receiver and Broadcaster contract. Broadcasters store messages; Receivers verify the Broadcasters' state on remote chains. To do this, a Receiver first verifies a chain of state commitment proofs to recover a remote state commitment, then verifies the Broadcaster's state at that commitment.
Critically, the logic for verifying state commitment proofs is not hardcoded in the Receiver. Instead, it delegates this to a user specified list of StateProver contracts. Each StateProver defines how to verify a state commitment proof for a specific home chain to recover the state commitment of a specific target chain. Because the state commitment schemes and layouts of rollup contracts can change over time, the state commitment proof verification process itself must also be upgradeable—hence the StateProvers 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:
A state commitment is a bytes32 hash that commits to the state of a chain at a particular point in time. This commitment is produced by the rollup and serves as a cryptographic representation of the chain's state. The most common state commitments are:
The choice of state commitment depends on what the rollup commits to its parent chain. Different rollups may use different state commitments, and the StateProver is responsible for verifying proofs that recover these commitments from the parent chain's state.
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.
StateProvers prove a unidirectional link between two chains that have direct access to each other's finalized state commitments. The chains in this link are called the home chain and the target chain. StateProvers are responsible for verifying state commitment proofs to prove the existence of finalized target state commitments in the state of the home chain. Block hashes are the recommended state commitment, but implementations may use other commitments (e.g., state roots, batch hashes).
Since the StateProvers are unidirectional, each chain needs to have two:
StateProvers MUST ensure that they will have the same deployed code hash on all chains.
StateProvers can be used to get or verify target state commitments, 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 StateProverPointer is a Pointer to a StateProver which can be updated if proving logic needs to change.
StateProverPointers are used to reference StateProvers as opposed to referencing Provers directly. To that end, wherever a StateProver is deployed a StateProverPointer needs to be deployed to reference it.
StateProverPointers 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 StateProverPointer ownership and updates.
When updating a StateProverPointer to point to a new StateProver implementation:
StateProverPointers MUST store the code hash of the StateProver implementation in slot STATE_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 StateProverPointers. Receivers use the StateProvers that the Pointers reference to verify a series of proofs to obtain the remote chain's state commitment. A route works with any state commitment scheme (block hashes, state roots, etc.) and is 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]).StateProverCopies are exact copies of StateProvers deployed on non-home chains. When a StateProver code hash is de-referenced from a Pointer, a copy of the StateProver 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 stateProverPointerId => IStateProver stateProverCopy) to keep track of StateProverCopies.
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.IStateProverPointer(0xA)::implementationAddress to get the address of StateProver L->MIStateProver(Prover L->M)::getTargetStateCommitment, passing input given by Subscriber to get a state commitment of chain M.IStateProver(Prover Copy M->P)::verifyTargetStateCommitment, passing chain M's state commitment and proof data by Subscriber to get a state commitment of chain P.IStateProver(Prover Copy P->R)::verifyTargetStateCommitment, passing chain P's state commitment and proof data by Subscriber to get a state commitment of chain R.IStateProver(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 SHOULD use storage proofs to read messages from storage slots. However, an alternative method 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 proofs and instead lookup the result. As an example we can consider the route between two L2s using storage proofs:
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 state commitments 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 state commitments. To accommodate this diversity, StateProvers implement chain-specific procedures. This flexibility allows integration with each rollup's distinct architecture and state commitment scheme.
The StateProver handles the final step of verifying a storage slot given a target state commitment to accommodate rollups with differing state commitment schemes and formats.
Routes reference StateProvers through Pointers rather than directly. This indirection is crucial because:
Since StateProverPointers reference StateProvers via their code hash, a copy of the StateProver 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 StateProver 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 StateProver's verifyTargetStateCommitment or getTargetStateCommitment functions might return data besides a finalized target state commitment, then invalid messages could be read by a Receiver. For instance, if a chain stores its state commitments on the parent chain in a specific mapping, and that storage slot is later repurposed, then an old StateProver might be able to pass along an invalid state commitment. It is therefore important that either:
A malicious StateProverPointer owner can DoS or forge messages. However, so can the chain owner responsible for setting the slot of historical parent/child state commitments. Therefore it is expected that this chain owner be the same as the owner of the StateProverPointer so as not to introduce additional risks.
If an owner neglects their responsibility to update the Pointer with new StateProver implementations when necessary, messages could fail to reach their destinations.
If an owner maliciously updates a Pointer to point to a StateProver 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 StateProver, an unowned StateProverPointer 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 state commitments at each step in the route.
Copyright and related rights waived via CC0.