Skip to Content
Receiving Messages

Receiving Messages on Reactor

Blockchain applications that receive messages from Reactor will need to implement the execute function. The execute function is where application logic, like validating the message sender and source, takes place. It also responsible for parsing the data from the message’s payload and performing tasks based on the application’s use cases.

Execute Function

function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external;

Execute message parameters

  • _sourceName: Name of source where the message came from.
  • _sourceAddress: Address for the sender of the message on the source.
  • _action: Type of message that was received.
  • _payload: Encoded action-specific data for the message.

Source Name

Source Name is one of the Supported Names which are unique identifiers that conveys the sending source of the message. This allows the receiving application to add checks to ensure that it process messages that originate on known sources.

Source Address

The address of the sender from the source. The receiving application can use the source address to further check via a whitelist to verify that a message is coming from a trusted source.

Action

Actions are the way that convey to Reactor the intent behind the message that is being received. This allows Reactor to be simple and flexible to cover the workflows required today while staying open to enable the emerging use cases in the future.

The following actions are currently supported:

  • chain_message: Send data between blockchains.
  • web_message: Get or post data on a web resource (i.e. web service or API).
  • generate_random_numbers: Create a set of verifiable random numbers.

Although these are the only supported actions at the moment, receiving applications should anticipate new actions in the future. The choice is up to the implementer of the receiving application, but taking an approach that allows a receiving application to grow as more scenarios are enabled by Reactor allows the application to evolve to adapt to changing requirements.

Note: When an action is being executed on the destination, it must complete within the max gas limit allocated to the transaction. For EVM destinations, this limit is 2M (2000000) gas units. Each individual action can have its own independent max gas limit per destination that can be higher than the default specified. Messages that fail due to out of gas errors are treated as successfully completed messages in Reactor and will not be eligible for retries. When designing your application, be sure to take into consideration this limit when reasoning through your scenaios.

Payload

On Reactor, any data that can be encoded into bytes can be delivered as the payload for a message. It is the job of the receiving application to parse the payload and extract the needed data. This can be as simple as decoding the payload via abi’s decode function or a custom decoding scheme that meets the requirements of the application. While chain_message or web-to-chain messages can contain any byte-encoded data, messages like web_messageand generate_random_numbers will follow a predefined format. In the case of web_message messages, the payload received will be an ABI encoded value with the HTTP response code and the response body as message parameters. The names and values of the response headers will not be sent, but should be accounted for when decoding they web response payload. Returning response headers may be enabled in the future or by request. An example of how to parse a web_message message is below. For generate_random_numbers response messages, the payload will be an ABI encoded array of integers containing the number of random numbers requested. An example of how to parse a generate_random_numbers message is below for reference.

Message Parameters

At times, there is a need to convey additional information related to a message between Reactor and the applications. In these scenarios, like request and response headers for web_message messages, Reactor payloads can be encoded with parameters.

// Example of web request with headers bytes memory web_message_payload = abi.encode("GET", "https://docs.reactor.network", ""); bytes memory payload = abi.encode(web_message_payload, ["header.NAME_1", "header.NAME_2"], ["VALUE_1", "VALUE_2"]);

For web_message messages, the payload delivered to the application at the destination address on the destination will only include the HTTP status code and the HTTP response body. No response headers will be included at this time. Since any data that is delivered to the contracts will be persisted to the blockchain, it is important to handle sensitive data in a secure manner. For these cases, please contact the Reactor Team so that overrides can be added to remove any sensitive data before it is sent to any applications. In the future, there will be a self-service portal that will allow for the management of sensitive data like API keys, credentials, or other private information for messages coming into Reactor and going out of Reactor. This includes using this sensitive data in outgoing web_message messages.

Security Considerations

Reactor guarantees delivery and integrity of messages, but your application is responsible for deciding whether a given message is allowed to have an effect.

At a high level:

  • Reactor Core observes messages originating on the source.

  • It forwards those messages to the destination by calling your application through the ReactorGateway via the IExecutable interface’s execute function implemented in your application.

  • The execute call you receive includes:

    • _sourceName – The source this message came originated on.
    • _sourceAddress – Who initiated it on that source.
    • _action – What they task they want to perform.
    • _payload – The data for that action.

The Reactor Gateway enforces:

  • The message was actually observed on a source chain.
  • The bytes delivered in _payload match what was in the message sent from the source (i.e. the message was not tampered with and its integrity is intact).
  • The same message (same canonical ID and metadata) cannot be executed twice on the destination.

However, the Gateway does not automatically decide whether the message is authorized to change your application’s state. That’s on you.

Concretely, you MUST enforce at least the following in your execute implementation:

1. Only trust calls that come from the Reactor Gateway

Your application should never accept a direct call to execute from an EOA or any random application.

modifier onlyReactorGateway() { require(msg.sender == reactorGatewayAddress, "unauthorized caller"); _; } function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { // ... }

If you do not gate msg.sender, anyone can call your application’s execute and spoof messages.

2. Check that the source is trusted

Anyone on the source can publish a message. Reactor will deliver it, but that does not mean you want to honor it.

You should explicitly verify that the message is coming from a source you trust, and (optionally) from a sender you trust on that source:

function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { // Example: allow only messages that originated from a specific application // on a specific source. bool validSource = keccak256(bytes(_sourceName)) == keccak256(bytes("ethereum")); bool validSourceSender = keccak256(bytes(_sourceAddress)) == keccak256(bytes("0xYourAppOnEthereum")); require(validSource && validSourceSender, "untrusted source"); // ... }

Recommended patterns:

  • Maintain an allowlist of approved _sourceName values (e.g. "ethereum", "arbitrum_one", "base", etc.).
  • Maintain an allowlist of approved _sourceAddress values per _sourceName.
  • Reject anything else.

This prevents a malicious application on Source A from sending a message that pretends to control privileged logic on Destination B.

3. Authorize by action

_action is the intent of the message (e.g. chain_message, web_message, generate_random_numbers, or future actions).

Your application should only run sensitive logic for known and allowed actions. Everything else should revert.

bytes32 private constant ACTION_CHAIN_MESSAGE = keccak256("chain_message"); bytes32 private constant ACTION_WEB_MESSAGE = keccak256("web_message"); function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { // 1. Validate source require(_isTrustedSource(_sourceName, _sourceAddress), "untrusted source"); // 2. Route based on approved actions bytes32 actionHash = keccak256(bytes(_action)); if (actionHash == ACTION_CHAIN_MESSAGE) { _handleChainMessage(_payload); } else if (actionHash == ACTION_WEB_MESSAGE) { _handleWebMessage(_payload); } else { revert("unsupported action"); } }

Why this matters: Without this gate, an attacker on the source could call Reactor with _action = "chain_message" and a payload that tells your destination application to mint tokens, even if your dapp never intended to expose minting via Reactor.

4. Validate and decode the payload safely

Once you’ve established:

  1. the caller is the real ReactorGateway,
  2. the message came from a trusted _sourceName / _sourceAddress,
  3. the _action is something you support,

then (and only then) you should decode _payload and perform state changes.

For example:

function _handleChainMessage(bytes calldata _payload) internal { // Your own ABI. For example: (address recipient, uint256 amount) (address recipient, uint256 amount) = abi.decode(_payload, (address, uint256)); // Optional final checks before mutating state require(recipient != address(0), "bad recipient"); require(amount > 0 && amount <= MAX_MINT_PER_MESSAGE, "bad amount"); _mint(recipient, amount); }

Do not assume _payload is “well formed” just because it arrived via Reactor. Treat it like untrusted input until you’ve validated everything above.

5. Plan for new actions and new sources

Reactor is designed to support new actions and new sources over time. Your authorization logic should make that explicit.

Some examples:

  • Default-deny: revert on any _sourceName / _action you haven’t explicitly allowlisted yet.
  • Versioned allowlists: keep a mapping in storage so you can upgrade what is allowed via governance instead of hardcoding everything permanently.

Putting it all together

Your application should treat Reactor as a transport layer with replay protection, not as an authorization oracle.

Reactor ensures:

  • The message really existed on a source.
  • The payload was not tampered with in transit.
  • The message can’t be executed more than once on the destination.

Your application must still ensure:

  • The message is coming from a source you trust (_sourceName, _sourceAddress, msg.sender).
  • The message is asking to do an action you explicitly allow (_action).
  • The message data is sane for that action (_payload decoding and validation).

If you skip these checks, a malicious sender on any supported source could ask Reactor to call your application with arbitrary data and potentially trigger privileged behavior. If you include these checks, you get clean cross-chain / web-to-chain / chain-to-web messaging with strong safety guarantees.

Examples

Here’s are three basic examples of implementing the execute function for a message receiving application:

Chain Message

// Example of receiving a chain_message message function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { sourceName = _sourceName; sourceAddress = _sourceAddress; action = _action; payload = _payload; last_message = abi.decode(_payload, (string)); emit ExecutableCrossChainMessageReceiverSuccess(_sourceName, _sourceAddress, _action, keccak256(_payload)); }

Web Message

// Example of receiving a web_message message function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { sourceName = _sourceName; sourceAddress = _sourceAddress; action = _action; payload = _payload; (last_http_response_status_code,last_http_response_body, last_http_response_header_names, last_http_response_header_values) = abi.decode(_payload, (uint256, string, string[], string[])); emit ExecutableWebRequestReceiverSuccess(_sourceName, _sourceAddress, _action, keccak256(_payload)); }

Generate Random Numbers Message

// Example of receiving a generate_random_numbers message function execute( string calldata _sourceName, string calldata _sourceAddress, string calldata _action, bytes calldata _payload ) external onlyReactorGateway { sourceName = _sourceName; sourceAddress = _sourceAddress; action = _action; payload = _payload; random_numbers = IReactorHelper(reactor_helper_address).decodeGenerateRandomNumbersResponseData(_payload); // Or: abi.decode(_payload, (uint256[])); emit ExecutableRandomNumberReceiverSuccess(_sourceName, _sourceAddress, _action, keccak256(_payload)); }

Last updated on