Messaging Properties
Message State
Messages are sent from the User Application (UA) at source srcUA
to the UA at the destination dstUA
. Once the message is received by dstUA
, the message is considered delivered (transitioning from INFLIGHT
to either SUCCESS
or STORED
)
Message State | Cases |
---|---|
INFLIGHT | After a message is sent |
SUCCESS | A1: dstUA success OK() A2: dstUA fails with caught error/exception |
STORED | B1: dstUA fails with uncaught error/exception |
// message handling at destination chain
try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) {
// message state becomes SUCCESS
} catch {
// message state becomes STORED
emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _payload);
}
Case A2:
dstUA
is expected to store the message in their contract to be retried (LayerZero will not store any successfully delivered messages). dstUA
is expected to monitor and retry STORED
messages on behalf of its users.
Case B1:
dstUA
is expected to gracefully handle all errors/exceptions when receiving a message, and any uncaught errors/exceptions (including out-of-gas) will cause the message to transition into STORED
. A STORED
message will block the delivery of any future message from srcUA
to all dstUA
on the same destination chain and can be retried until the message becomes SUCCESS
. dstUA
should implement a handler to transition the stored message from STORED
to SUCCESS
. If a bug in dstUA
contract results in an unrecoverable error/exception, LayerZero provides a last-resort interface to force resume message delivery, only by the dstUA
contract.
Message Ordering
LayerZero provides ordered delivery of messages from a given sender to a destination chain, i.e. srcUA -> dstChain
. In other words, the message order nonce is shared by all dstUA
on the same dstChain
. That's why a STORED
message blocks the message pathway from srcUA
to all dstUA
on the same destination chain. If it isn't necessary to preserve the sequential nonce property for a particular dstUA
the sender must add the nonce into the payload and handle it end-to-end within the UA. UAs can implement a non-blocking pattern in their contract code.
Extensibility
Message Adapter Parameters
LayerZero allows UAs to add arbitrary transaction params in the send()
function, providing a high level of flexibility and opening up opportunities for a diverse set of 3rd party plugins This is implemented as an unreserved byte array parameter to the send()
function, with UAs allowed to write any additional data necessary into that parameter. We recommend that UAs leave some degree of configurability for the extra parameters to allow for feature extensions.
One great feature of _adapterParams
is performing an Airdrop.
Patterns
Non-Reentrancy
LayerZero Endpoint has a non-reentrancy guard for both the send()
and receive()
, respectively. In other words, both send()
and receive()
can not call themselves on the same chain. UAs should not rely on LayerZero to perform the non-reentrancy check. However, UAs can query the endpoint to see if the endpoint isSendingPayload()
or isReceivingPayload()
for finer-grained reentrancy control.
Message Chaining
UAs can call send() in the receive() calls on the same chain. Example applications for calling send() in the receive() include (e.g. Ping Pong):
- the UA at the source chain wants a message receipt (Chain A -> Chain B -> Chain A)
- the UA at the destination reroutes the message (Chain A -> Chain B -> Chain C)
function lzReceive(uint16 _srcChainId, bytes memory _fromAddress, uint64, /*_nonce*/ bytes memory _payload) external override {
...
// message chaining
endpoint.send{value: messageFee}(
...
);
}
However, the fee for sending messages on another chain is not observable on-chain. UAs would need to create some fee estimate heuristics. Optionally, user apps can store the chained message and then resend them with another transaction.
Multi-Send
UAs can send multiple messages in one transaction at the source chain. The endpoint non-reentrancy will not block this pattern.
function sendFirstMessage(
uint gasAmountForDst, uint16[] calldata chainIds, bytes[] calldata dstAddresses) external payable {
...
for(uint i = 0; i < chainIds.length; i++){
endpoint.send{value: fee}(chainIds[i], dstAddresses[i], messageString, msg.sender, address(0x0), _relayerParams);
}
}