Contents

LayerZero V2 Part 2: How a Message Travels

This is Part 2 of a series on LayerZero V2. Part 1 covers the basics.

/images/part2-message-lifecycle.svg

Let’s trace a message from Chain A to Chain B, step by step.

Phase 1: Sending (Chain A)

Step 1:  Your app calls send() on the OFT contract
Step 2:  OFT burns/locks tokens and builds the message
Step 3:  OFT calls _lzSend() → EndpointV2.send()
Step 4:  Endpoint routes to SendUln302 (the MessageLib)
Step 5:  SendUln302 encodes the message into a Packet
Step 6:  SendUln302 collects fees (DVN + Executor + treasury)
Step 7:  SendUln302 emits an event with the packet data

At this point, the message exists on Chain A as an event log. Nothing has happened on Chain B yet.

The Packet Structure

Every message becomes a Packet:

struct Packet {
    uint64  nonce;      // Sequential message counter
    uint32  srcEid;     // Source chain endpoint ID
    address sender;     // Your contract on Chain A
    uint32  dstEid;     // Destination chain endpoint ID
    bytes32 receiver;   // Your contract on Chain B (bytes32 for non-EVM)
    bytes32 guid;       // Globally unique ID for this message
    bytes   message;    // The actual payload
}

The guid is generated deterministically from the nonce, source, and destination. It uniquely identifies every message across all chains.

Phase 2: Verification (Off-Chain)

Step 8:  DVNs monitor Chain A for LayerZero events
Step 9:  Each DVN independently verifies the message
Step 10: Each DVN submits a verification attestation on Chain B

The DVNs work independently. They don’t talk to each other. Each one separately confirms “yes, this message is real” by:

  1. Checking the block header on Chain A
  2. Confirming the transaction exists
  3. Validating the packet data
  4. Waiting for the required block confirmations (e.g., 15 for Ethereum, 5 for L2s)
  5. Submitting a payloadHash attestation on Chain B

Phase 3: Execution (Chain B)

Step 11: ReceiveUln302 checks: have enough DVNs verified?
Step 12: Executor calls EndpointV2.lzReceive()
Step 13: Endpoint calls your app's _lzReceive() → tokens minted/unlocked

The ReceiveUln302 checks two conditions:

  • All required DVNs have verified ✓
  • At least threshold optional DVNs have verified ✓

Only then can the message be delivered.

The Whole Flow

Chain A                    Off-Chain                  Chain B

App.send()
    ↓
Endpoint.send()
    ↓
SendUln302
    ↓ (emit event)
                        DVN 1 verifies ──→ attest on B
                        DVN 2 verifies ──→ attest on B
                        DVN 3 verifies ──→ attest on B
                                                  ↓
                                          ReceiveUln302
                                          (quorum met?)
                                                  ↓
                        Executor ──────→ Endpoint.lzReceive()
                                                  ↓
                                          App._lzReceive()
                                          (mint tokens)

Nonce Management

LayerZero uses nonces to track messages. There are three types:

  • Outbound nonce: Incremented on every send. Sequential, never skipped.
  • Inbound nonce: Tracks which messages have been verified on the destination.
  • Lazy inbound nonce: Tracks the highest consecutively executed nonce.

Ordered vs Unordered Delivery

By default, messages can be executed in any order (unordered). But the nonce system still provides censorship resistance — all prior nonces must be verified before any nonce can execute.

For strict ordering, add addExecutorOrderedExecutionOption() to your options. This forces messages to execute in nonce order — but if message N fails, N+1, N+2… are blocked until N is resolved.

When Things Go Wrong

A Message Fails to Execute

The message is still verified. Anyone can retry by calling lzReceive() directly with the correct parameters. The Executor has no monopoly.

A Message Is Stuck

Recovery options:

Action What It Does Who Can Call
Retry Re-execute a failed message Anyone
Skip Skip a stuck nonce (message is lost) OApp owner
Clear Clear a stuck compose message OApp owner
Nilify Invalidate a maliciously verified message OApp owner
Burn Clean up nilified state OApp owner

Latency

How long does the whole process take?

  • L2 → L2 (1 DVN): ~1-2 minutes
  • Typical (2-3 DVNs): ~5-15 minutes
  • High security (5+ DVNs): ~15-30 minutes

Latency = max(source finality wait, DVN verification time, destination execution)

The bottleneck is usually waiting for block confirmations on the source chain (15 blocks on Ethereum ≈ 3 minutes).

Next