diff --git a/specs/message-passing/attributes-vs-extradata.md b/specs/message-passing/attributes-vs-extradata.md new file mode 100644 index 0000000..249c177 --- /dev/null +++ b/specs/message-passing/attributes-vs-extradata.md @@ -0,0 +1,46 @@ +# bytes[] attributes vs bytes extraData + +_To see the whole picture of the proposed changes for message passing standard, please refer [here](./message-passing-unified-specs.md)._ + +## extraData Field + +Instead of requiring structured arrays of attributes, we propose to use a single `bytes extraData` field to encode optional gateway-specific metadata. + +```solidity +// Optional encoded gateway metadata (leave empty if unused) +bytes calldata extraData +``` + +- `extraData` is an opaque blob interpreted by the origin gateway. +- It MAY contain data related to the underlying messenger logic such as gas limits, instructions, callback options, or any other protocol-specific configurations. +- If unused, `extraData` should be left empty. +- Optionally the Gateways could expose a view function that decodes their `extraData` format. This is a function defined in the `IGateway` interface but a nice to have of the Gateway implementations. + +This approach offers the following advantages: + +- **Simplicity**: The `sendMessage` interface remains minimal and easy to implement. +- **Flexibility**: Each gateway or adapter can define and interpret `extraData` freely, without requiring standardized attribute formats. +- **Gas Efficiency**: Encoding and decoding a single blob of bytes is cheaper than parsing an array of structured attributes. +- **Future Extensibility**: New metadata types can be supported within `extraData` without changing the interface. +- Attributes force upgrades whenever the underlying messenger changes, as their structure must be explicitly supported. In contrast, `extraData` avoids this need by being fully opaque and simple, allowing changes without requiring interface upgrades. + +It is recommended that gateways and adapters document their expected `extraData` formats if interoperability between adapters is desired. + +>💡 +>**Non-EVM Friendliness:** +>Using a single opaque `extraData` field improves compatibility with non-EVM chains, which may not natively support ABI encoding. It ensures that both EVM and non-EVM ecosystems can adopt the standard naturally, using their preferred serialization methods. +>This design enables cross-chain adapters to interpret metadata according to the serialization standards of their respective ecosystems without requiring translation of EVM-specific ABI structures. + +## Comparison + +| Aspect | `bytes[] attributes` | `bytes extraData` | +| --- | --- | --- | +| Format | Structured array (each entry = ABI function call) | Single opaque blob | +| Structure | Complete structured | Still structured, but it’s “proprietary” structured by the underlying messenger | +| Flexibility | High, but depends on standardizing attribute formats | Very high, gateway-specific interpretation/implementation | +| Simplicity | More complex for developers (attribute definition and parsing) | Simpler for developers (optional opaque data) | +| Composability | Facilitates standardized cross-bridge features | Relies on bridge or adapter-level documentation and agreement | +| Standardization level | can be standardized with new ERCs | the content is hard to standardize | +| Compatibility between them | cannot support opaque bytes extraData | can be encoded from bytes[] attributes | +| Gas Usage | Higher (decoding multiple structured elements) | Lower (decoding a single blob) | +| Spec Complexity | Higher (attribute standardization and management required) | Lower (no structure mandated by the interface) | diff --git a/specs/message-passing/hook-execution-design.png b/specs/message-passing/hook-execution-design.png new file mode 100644 index 0000000..1cf5c3f Binary files /dev/null and b/specs/message-passing/hook-execution-design.png differ diff --git a/specs/message-passing/hooks-explainer.md b/specs/message-passing/hooks-explainer.md new file mode 100644 index 0000000..f85b4d9 --- /dev/null +++ b/specs/message-passing/hooks-explainer.md @@ -0,0 +1,167 @@ +# Hooks Explainer + +_To see the whole picture of the proposed changes for message passing standard, please refer [here](./message-passing-unified-specs.md)._ + +## What are Hooks? + +**Hooks are extra actions that can run during the message flow.** They are composable and can change how things work without changing the messaging protocol. + +The gateway/messenger invokes the hook after sending a message, passing only `(address hook, bytes payload)`. Because the call comes from the gateway, the hook can trust the context and, if needed, query the gateway for more details about the current message. + +One valuable feature is that hooks can call other hooks, enabling *hook* action chaining. Also, hooks can be an aggregation of other hooks enabling, composition. + +## Primary Use Case: Payment for relayers + +When sending a cross-chain message, a relayer is typically required to deliver the message from the origin to the destination chain. + +To incentivize relayers and guarantee delivery, it is often necessary to commit a fee payment (which is generally calculated from expected destination gas limit + relayer commercial fee) at the moment of sending the message. + +For example by including the `hookData` field in `sendMessage`, in `Hyperlane`, users can atomically **pay the** `RelayerPaymentsContract` at the time of sending the message. + +### Flow + +- `hook` points to the relayers payment contract address. +- `hookData` encodes the call to pay the gas fee (`payForGas(messageId, gasAmount, refundAddress)`). +- `value` includes the ETH (or native token) payment amount. + +### Example + +Suppose the `RelayerPaymentsContract` exposes the function: + +```solidity +function payForGas(bytes32 messageId, uint256 gasAmount, address refundAddress) + external payable; +``` + +The `hookData` passed to `sendMessage` would be: + +```solidity +HookData({ + hook: address(RelayerPaymentsContract), + hookPayload: abi.encodeCall( + RelayerPaymentsContract.payForGas, + (messageId, 300_000, userAddress) + ), + value: 0.02 ether +}); +``` + +Thus, the flow would be: + +- First, the cross-chain message is sent. +- Then, the hook is executed, paying the gas fee to the relayers’ payment contract. + +### Benefits + +- **Atomicity**: Gas payment and message sending happen in a single transaction. +- **Improved UX**: Users do not need to manually perform two transactions (pay + send). +- **Flexibility**: Gateways can support various gas payment systems without hardcoding them. +- **Optionality**: If no hook is provided, no gas prepayment occurs. + + +## Additional Use Cases for Hooks (Origin Chain) + +Even beyond the relayer’s gas payment, `hookData` enables powerful extensibility in the origin chain: + +### 1. Validation of Conditions + +When sending a message, the hook could validate whether certain preconditions are satisfied (e.g., minimum balance checks, allowlist verifications). + +If the validation fails, the transaction can revert immediately. + +### 2. Bundling/Batching Multiple Messages + +Hooks can package several cross-chain messages into an atomic bundle to treat them as one logical operation on the destination side. Each subsequent message references the previous one through a hook logic. + +### 3. Notifying Other Contracts + +The hook could inform or synchronize state with external systems (e.g., registering the outgoing message in an Message Manager contract). + +### 4. Setting Up Timeout or Expiration Mechanisms + +The hook can register a timeout or expiration condition associated with the message before dispatch. + +**Example:** + +- Before sending the message, the hook inserts the message ID and a timestamp into a `TimeoutManager` contract. +- Automated fallback logic can trigger if the message is not delivered or finalized within a certain timeframe (e.g., refunds, invalidations, retries). + +**Why it matters:** + +- Protects users against messages that could remain stuck indefinitely due to network failures, relayer issues, or malicious actors. +- Enables cleaner lifecycle management by ensuring stale or abandoned messages can be handled without manual intervention. + +### 5. Callbacks-async execution + +A message can easily specify via an async/callback-hook how to continue execution, e.g., in the case of a cross-chain read/view. + + +## General Benefits of `hookData` + +- **Composable Execution**: Additional operations can be seamlessly combined with sending an intent. +- **Improved UX**: Users execute complex workflows atomically in one atomic call. +- **Future-Proof Design**: New use cases can be enabled without modifying the `sendMessage` core logic. Also the application layer can add business logic into the underlying messenger to be executed within the `sendMessage` call. +- **Optional by Nature**: Hooks are not mandatory — default behavior remains simple for users who do not require additional actions. +- Create dynamic instructions determined in runtime. + +# Hook Execution Design + +In order to ensure secure, controlled, and auditable execution of hooks, Gateways SHOULD delegate the hook execution to a dedicated `HookExecutor` contract instead of executing the hook directly. + +## Rationale + +- **Minimized Attack Surface**: The Gateway remains clean and focused on message dispatching only. +- **Centralized Security Checks**: Gas limits, address whitelisting, and value forwarding validations can be handled uniformly. +- **Future Upgradeability**: Execution models can evolve without changing the Gateway core logic. + +## Proposed Model + +- The `Gateway` validates the existence of `hookData`. +- The `Gateway` calls the `HookExecutor`, forwarding `hookData` and `msg.value` as needed. +- The `HookExecutor` executes the hook, ensuring that: + - If the hook call fails, the transaction reverts. + - Gas forwarding can be capped if necessary. + +![hook-execution-design](./hook-execution-design.png) + +## Example: Minimal Hook Executor + +```solidity +contract HookExecutor { + function executeHook(IGateway.HookData calldata hookData) external payable { + require(hookData.hook != address(0), "No hook address provided"); + + (bool success, ) = + hookData.hook.call{value: hookData.value}(hookData.hookPayload); + require(success, "Hook execution failed"); + } +} +``` + +- **Optional**: Implement `onlyGateway` access control to restrict who can trigger `executeHook`. + +## Additional Notes + +- Whether the hook is executed **pre-dispatch** or **post-dispatch** is left to the Gateway implementation. The interface itself remains agnostic. +- By default, hook failures **revert the entire transaction** to maintain safety and message atomicity. + +## Comparison + +| Aspect | Without Hooks | With Hooks | +| --- | --- | --- | +| **Extensibility (New Features)** | Hard to evolve; any new needs require gateway, or application contract upgrades. | New behaviors (e.g., payment models, service registrations) can be plugged via new hook contracts. | +| **Complexity at Gateway Level** | Gateway must implement all possible behaviors internally. | Gateway remains clean, delegating optional behaviors to external hook contracts. | +| **Transaction Atomicity** | Multi-step workflows require multiple user transactions. | Full workflows (fund, authorize, register) can happen atomically in one transaction. | +| **Optionality for Users** | Gateway has to generalize or restrict for all cases. | Hooks are purely optional; users choose to use them or not. | +| **Upgradability and Modularity** | Gateway becomes a bottleneck for protocol upgrades. | Hooks allow adding new features without touching the Gateway itself. | +| Payment for Relayers | Requires separate transactions or implicit integration in the Gateway. | Can be atomically prepaid at message dispatch time via a flexible, modular hook. | +| **Timeout and Expiration Handling** | Requires manual timeout systems or post-processing. | Can pre-register timeouts tied to message IDs, enabling automatic lifecycle management. | +| **Collateralization or Fund Locking** | Requires external pre-processing or off-chain coordination. Or a separated call after or before sending the message. | Funds can be locked atomically during the sendMessage flow through hooks. | + +# Conclusion + +Including `hookData` in the `sendMessage` interface unlocks atomic, modular, and future-proof workflows for cross-chain messaging, while keeping the core Gateway logic minimal, focused, and secure. + +Hooks allow optional pre- and post-dispatch operations — such as fee prepayment, actor authorization, fund locking, or timeout registration — without forcing all users or messages to adopt the same processing flow. + +By delegating auxiliary behaviors to external hook contracts, the protocol gains flexibility, extensibility, and safer upgrade paths, ensuring that evolving ecosystem needs can be supported without breaking core infrastructure. diff --git a/specs/message-passing/message-passing-unified-specs.md b/specs/message-passing/message-passing-unified-specs.md new file mode 100644 index 0000000..76cc91e --- /dev/null +++ b/specs/message-passing/message-passing-unified-specs.md @@ -0,0 +1,223 @@ +# Cross Messaging Unification + +*This document aims to foster collaboration on existing standard specifications, without prescribing whether the best path forward involves evolving current ERCs or considering complementary directions.* + +## Context + +We propose a “unified” cross‑chain‑messaging interface that starts from [ERC‑7786](https://github.com/ethereum/ERCs/blob/448902e83117f175b4f682fb2d60545709df8cef/ERCS/erc-7786.md) and borrows useful ideas from [ERC‑6170](https://github.com/ethereum/ERCs/blob/448902e83117f175b4f682fb2d60545709df8cef/ERCS/erc-6170.md), [ERC‑7854](https://github.com/ethereum/ERCs/pull/817), and the [Interop Working Group discussions](https://github.com/ethereum/L2-interop/pull/13). + +The primary audience is application developers, who need a predictable, ergonomic API rather than a patchwork of bridge‑specific contracts. + +This draft keeps 7786’s proven core but adds just enough opinionated structure—notably binary interoperable addresses and optional hooks—to make implementations consistent and developer‑friendly. + +👉 **This unified version is open for discussion and collaboration, and we welcome feedback and contributions from the community.** + +--- + +## Specification + +A complete specification primarily consists of four logical building blocks: + +1. **Gateway:** The canonical entry point API for the cross‑chain message flow. On the origin chain, it lets contracts send messages, emits a tracking event, and (optionally) invokes hook calls. On the destination chain, each implementation provides its own Destination Gateway, which might be responsible for verifying the proof and delivering the message to the recipient. +2. **Recipient Interface**: A minimal contract API that must be implemented by any contract wishing to receive cross-chain messages. +3. **Message**: The cross-chain data structure containing sender, recipient, payload, and implementation-specific extra data required by the underlying bridge to handle delivery or verification. +4. **Hooks**: Opt‑in extension points that let developers run arbitrary logic before or after the sending, without polluting the Gateway surface. + +### Message Field Encoding + +A cross-chain message includes sender and recipient addresses, a payload, extra data, and optional hooks. + +### Sender and Recipient + +Binary interoperable addresses (see [ERC‑7930](https://github.com/ethereum/ERCs/pull/1002)), containing the account address and chain identifier (from CAIP‑2). + +### Payload and Extra Data + +The payload is an opaque `bytes` value. The Extra Data encodes optional logic or metadata (such as low-level calls) that the underlying messaging protocol should process during delivery. + +### Hooks + +A hook is any contract logic that the application developer wants to execute after a message is sent from the origin chain. They make it possible for developers to include any external logic apart from the underlying message protocol entry point when sending a message. Hooks are encapsulated under a struct that contains the hook payload, and the local address of the hook, and value. Hooks are never part of the message delivered to the recipient; they only affect the execution environment on the origin chain. + +See more on Hooks [here](./hooks-explainer.md). + +### Interfaces + +Smart contracts interact with a `Gateway` to send and receive messages. Using hooks (`HookData`) is optional. + +```solidity +interface IGateway { + // Optional Hook data struct + struct HookData { + bytes hookPayload; // Low-level call data: selector and parameters for the hook + ILocalAddress hook; // Local origin chain address + uint256 value; // Optional, amount of native token forwarded to the hook + } + + // MessageSent event + event MessageSent( + bytes32 outboxId, // Unique outbox ID (may be 0 if the gateway doesn’t track) + bytes sender, // Binary Interoperable Address + bytes recipient, // Binary Interoperable Address + bytes payload, // Message content + bytes extraData, // Optional encoded gateway metadata (leave empty if unused) + HookData hookData // Optional Hook data + ); + + // sendMessage function + function sendMessage( + bytes calldata recipient, // Binary Interoperable Address + bytes calldata payload, // Message content + bytes calldata extraData, // Optional encoded gateway metadata (leave empty if unused) + HookData calldata hookData // Optional Hook data + ) external payable returns (bytes32 outboxId); // Unique ID (may be 0 if the gateway doesn’t track) +} +``` + +The destination `IGateway` is out of scope in this specification. Its responsibility is to deliver the message to the recipient, which must implement the `IMessageRecipient` interface. + +```solidity +interface IMessageRecipient { + function receiveMessage( + bytes32 messageId, // Unique ID supplied by the destination gateway + bytes sender, // Binary Interoperable Address representation + bytes payload // Message content + ) external payable; +} +``` + +# Flows + +A first glance on the existing and proposed flow. + +## (As Reference) ERC-7786 flow: + +```mermaid +sequenceDiagram + participant User + participant GatewaySource as IERC7786GatewaySource (Chain A) + participant Relayer as Relayer / Post‑Processing + participant GatewayDestination as Destination Gateway (Chain B) + participant Receiver as IERC7786Receiver + + User->>GatewaySource: sendMessage(destinationChain, receiver, payload, attributes) + GatewaySource->>GatewaySource: emit MessagePosted(outboxId, sender, receiver, payload, value, attributes) + Note over Relayer: The post-processing or
relaying step triggers eventually + Relayer->>GatewayDestination: preReceive(parameters) "This is ONLY illustrative" + GatewayDestination->>GatewayDestination: verify proof & parse message + GatewayDestination->>Receiver: executeMessage(messageId, sourceChain, sender, payload, attributes) + Receiver-->>GatewayDestination: returns IERC7786Receiver.executeMessage.selector +``` + +## (As Reference) ERC-7854 flow: + +```mermaid +sequenceDiagram + participant User + participant MailboxA as Mailbox (Chain A) + participant Relayer as Off-Chain Relayer + participant MailboxB as Mailbox (Chain B) + participant Recipient as IMessageRecipient + + User->>MailboxA: dispatch(destinationDomain, recipientAddress, body, customHookMetadata...) + MailboxA->>MailboxA: emit Dispatch (sender, destination, recipient, message) + MailboxA->>MailboxA: emit DispatchId (messageId) + MailboxA->>Relayer: Off-chain bridging flow + + Relayer->>MailboxB: process(_metadata, _message) + MailboxB->>MailboxB: verify() with ISM + MailboxB->>Recipient: handle(origin, sender, _message) + MailboxB->>MailboxB: emit Process (origin, sender, recipient) + MailboxB->>MailboxB: emit ProcessId (messageId) + +``` + +## Proposed flow: + +As well as above, this flow shows how it could work on top of the OP Stack message passing protocol. + +```mermaid +sequenceDiagram + participant User + participant GatewayA as Gateway (Chain A) + participant HookA as Hook (Chain A) + participant Relayer as Off-Chain Relayer + participant GatewayB as Gateway (Chain B) + participant Recipient as IMessageRecipient + + User->>GatewayA: sendMessage(recipient, payload, extraData, hookData) + GatewayA->>HookA: hookData + GatewayA->>GatewayA: MessageSent (messageId, sender, recipient, payload) + GatewayA-->>Relayer: Off-chain bridging flow + + Note over GatewayB: using deliverMessage as example method, destination Gateway is out of scope + Relayer-->>GatewayB: deliverMessage(metadata, message) + GatewayB->>GatewayB: runVerifications() + GatewayB->>Recipient: receiveMessage(messageId, sender, payload) + +``` + +```mermaid +graph TD + Relayer + + subgraph Origin Chain + GatewayE[Gateway] + Sender --"sendMessage(recipient, payload, extraData, hookData)"-->GatewayE + GatewayE --"run HookData()"-->HookContract + end + + + subgraph Destination Chain + _GatewayR_[Gateway] + Recipient + + _GatewayR_ --"_run verifications(☁️)_"--> _GatewayR_ + _GatewayR_ --"receiveMessage(messageId, sender, payload)"--> Recipient + end + Relayer --"_deliverMessage(messageId, sender, payload)_"--> _GatewayR_ + +``` + +## Rationale and Discussions + +This design is an incremental evolution based on ERC-7786 and other related standards, aligning progressively with what is considered best practice. + +**What stays the same** + +- **Unchanged core API:** send and receive messages behave exactly as in ERC-7786. Adapters would remain the same way. +- **Safety / Liveness expectations**: All guarantees listed in ERC-7786 are inherited unchanged. +- **Event-driven tracking**: an `outboxId` is still emitted on the origin chain, and a `messageId` is provided by the relaying/deliver step on the destination chain. + +**What is added** + +- **Hooks (optional):** A `HookData` struct lets a project plug in fee payment, logging, batched calls, callbacks or any other external logic without bloating the base call. +- **Future‑proof addressing:** Binary interoperable addresses ([ERC-7930](https://github.com/ethereum/ERCs/pull/1002)); 7786 plans to adopt the same format, so this is forward‑compatible. +- **Clear separation of concerns**: `extraData` tweaks behavior inside the gateway, while `HookData` runs code outside the gateway. + +`ExtraData` and `HookData` + +- **`extraData`**: small blobs consumed *inside* the gateway (relevant for adapters e.g., define the gas limit or a caller). +- **`hookData`**: full contract calls executed *outside* the gateway (e.g., pay a relayer, emit custom analytics, trigger another bridge). + +**Hooks Explainer** + +Refer to [this document which expands about Hooks](./hooks-explainer.md). + +**ExtraData vs attributes** + +Refeter to [this document which compare both approaches](./attributes-vs-extradata.md). + +# Appendix + +## High-level Delta + +| **Change** | ERC-7786 | ERC-7854 | Unification | **Why?** | +| --- | --- | --- | --- | --- | +| **Renames** | `Gateway`, `sendMessage`, `executeMessage` | `Mailbox`, `dispatch`, `process`, `handle` | `Gateway`, `sendMessage`, `receiveMessage` | Fresh terminology around more common conventions. | +| **Message Struct** | No explicit struct. Parameters are passed directly: `destinationChain` (or `sourceChain`), `receiver` (or `sender`), `payload`, `attributes`. | Contains: `version`, `nonce`, `origin`, `sender`, `destination`, `recipient`, `body`. | No explicit struct. `Parameters` are passed directly: `sender` (or `recipient`) `payload`, `extraData`, `HookData`, and `messageId` in destination. | Simpler surface; extra inputs stays in `extraData` or `HookData` instead of bloating the envelope. | +| **Chain identifier and addresses** | `string` based; follows CAIP rules. There is an intention to support binary representations of [interoperable addresses](https://github.com/ethereum/ERCs/pull/1002). | Uses `uint32` and `bytes32` without defined standards. | Under development binary interoperable address, in `bytes`. | Chain-agnostic and accommodate the address efforts in the interop group. | +| Message identifiers | Gateway generates an `outboxId` in origin and `messageId` to be used in the destination. | Defined in the struct with `nonce`. | `outboxId` (origin event) and `messageId` (destination callback). | Leaves identifier format to the transport layer while giving explorers two stable anchors. | +| **Events** | Only `MessagePosted`. | Not defined in the standard. It's worth to add Hyperlane’s implementation introduces `Dispatch`, `DispatchId`, `ProcessId`, and `Process` events. | Defined as `MessageSent` in the origin Gateway. | One standardised event makes indexing uniform and keeps the destination side stateless. | +| **Post sending messages / Hooks Declaration** | Under `attributes`, as an optional feature. | Part of `dispatch` as `calldata` with appointed interface. | Encapsulated under `Hookdata` struct, (`hook`, `hookPayload` and `value`). Executed outside the gateway. | Hooks give full flexibility without bloating the core API; gateways that don’t need them can ignore the field. | +| Verification step | Abstracted from the interface (out of scope) | Defined as an ISM. | Abstracted from the interface (out of scope) | Keeps this ERC small; verification frameworks evolve quickly and deserve their own track. |