Skip to main content
Version: Endpoint V2

Solana DVN and Executor Configuration

You may recall that you can configure send and receive settings for your OApp per pathway set.

The Solana method for setting configurations should feel very familiar for developers who have configured EVM OApp Security Stack and Executors.

LayerZero defines a pathway as any configuration where any two points (OApp on Chain A and OApp on Chain B), have each called the setPeer function and enabled messaging to and from each contract instance.

Every LayerZero Endpoint can be used to send and receive messages. Because of that, each Endpoint has a separate Send and Receive Configuration, which an OApp can configure per target Endpoint (i.e., sending to that target, receiving from that target).

Protocol V2 Light Protocol V2 Dark

For a configuration to be considered correct, the Send Library configurations on Chain A must match Chain B's Receive Library configurations for filtering messages.

info

In the diagram above, the Source OApp has added the DVN's source chain address to the Send Library configuration.

The Destination OApp has added the DVN's destination chain address to the Receive Library configuration.

The DVN can now read from the source chain, and deliver the message to the destination chain.

Checking Default Configuration

For commonly travelled pathways, LayerZero provides a default pathway configuration. If you provide no configuration prior to setting peers, the protocol will fallback to the default configuration.

The default configuration varies from pathway to pathway, based on the unique properties of each chain, and which decentralized verifier networks or executors listen for those networks.

A default pathway configuration, at the time of writing, will always have one of the following set within SendULN302.sol and ReceiveUlN302.sol as a Preset Configuration:

Security StackExecutor
Default Send and Receive ArequiredDVNs: [ Google Cloud, LayerZero Labs ]LayerZero Labs
Default Send and Receive BrequiredDVNs: [ Polyhedra, LayerZero Labs ]LayerZero Labs
Default Send and Receive CrequiredDVNs: [ Dead DVN, LayerZero Labs ]LayerZero Labs

info

What is a Dead DVN?

Since LayerZero allows for anyone to permissionlessly run DVNs, the network may occassionally add new chain Endpoints before the default providers (Google Cloud or Polyhedra) support every possible pathway to and from that chain.

A default configuration with a Dead DVN will require you to either configure an available DVN provider for that Send or Receive pathway, or run your own DVN if no other security providers exist, before messages can safely be delivered to and from that chain.


Other default configuration settings, like source and destination block confirmations, will vary per chain pathway based on recommendations provided by each chain.

To read the default configuration, you can call the LayerZero Endpoint's getEndpointConfig method to return the default send and receive configuration for that target OApp.

import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

const log = await OftTools.getEndpointConfig(
connection,
oftConfig, // your OFT Config PDA
peer.dstEid,
);

console.log(
log.sendLibraryConfig.ulnSendConfig.executor,
log.sendLibraryConfig.ulnSendConfig.uln,
log.receiveLibraryConfig.ulnReceiveConfig.uln,
);
tip

The create-lz-oapp npx package also provides a faster CLI command to return every default configuration for each pathway in your project!

npx hardhat lz:oapp:config:get:default

Coming soon for Solana!


The getConfig function will return you an array of values from both the SendLib and ReceiveLib's configurations.

{
maxMessageSize: 10000,
executor: PublicKey [PublicKey(AwrbHeCyniXaQhiJZkLhgWdUCteeWSGaSN1sTfLiY7xK)] {
_bn: <BN: 93c69e71c758b9a308dd542e1c7f6edbf4b2342a6b74a0ce955ea97675f85528>
}
}

{
confirmations: <BN: 64>,
requiredDvnCount: 1,
optionalDvnCount: 1,
optionalDvnThreshold: 1,
requiredDvns: [
PublicKey [PublicKey(6LeK4yvvoUm2Jsy72QtNdxtyxWNBrvjzf5QT6EAtFoiq)] {
_bn: <BN: 4f52a8854a5d9bec9c46c87956e41d95ef8ed83d8386684bdcf04e8de83916b2>
}
],
optionalDvns: [
PublicKey [PublicKey(GDFZAp7yae7b77PrYPg685NM3ur9ZzrUVbu3KEBgtkwc)] {
_bn: <BN: e202bb07e5dc5734267f28e2a96e297988128582190eda0a028114f8425f3d53>
}
]
}

{
confirmations: <BN: 64>,
requiredDvnCount: 1,
optionalDvnCount: 1,
optionalDvnThreshold: 1,
requiredDvns: [
PublicKey [PublicKey(6LeK4yvvoUm2Jsy72QtNdxtyxWNBrvjzf5QT6EAtFoiq)] {
_bn: <BN: 4f52a8854a5d9bec9c46c87956e41d95ef8ed83d8386684bdcf04e8de83916b2>
}
],
optionalDvns: [
PublicKey [PublicKey(GDFZAp7yae7b77PrYPg685NM3ur9ZzrUVbu3KEBgtkwc)] {
_bn: <BN: e202bb07e5dc5734267f28e2a96e297988128582190eda0a028114f8425f3d53>
}
]
}
info

The important takeaway is that every LayerZero Endpoint can be used to send and receive messages. Because of that, each Endpoint has a separate Send and Receive Configuration, which an OApp can configure by the target destination Endpoint.

In the above example, the default Send Library configurations control how messages emit from the Solana Endpoint to the BNB Endpoint.

The default Receive Library configurations control how the Solana Endpoint filters received messages from the BNB Endpoint.

For a configuration to be considered correct, the Send Library configurations on Chain A must match Chain B's Receive Library configurations for filtering messages.

Challenge: Confirm that the Solana Endpoint's Send Library ULN configuration matches the Ethereum Endpoint's Receive Library ULN Configuration using the methods above.

Custom Configuration

To use non-default protocol settings, the delegate (should always be OApp owner) should call set a sendLibrary, receiveLibrary, and setConfig per pathway from the OApp's Endpoint.

When setting your OApp's config, ensure that the Send Configuration for the OApp on the sending chain (Chain A) matches the Receive Configuration for the OApp on the receiving chain (Chain B).

For the Solana blockchain, you will also need to init the config accounts. See the example from the OFT Solana SDK.

Both configurations must be appropriately matched and set across the relevant chains to ensure successful communication and data transfer.

The examples below will explain how setConfig manages your OApp's configurations, and how to build the transactions necessary to add custom parameters.

info

The setDelegate function in LayerZero's OApp allows the contract owner to appoint a delegate who can manage configurations for both the Executor and ULN. This delegate, once set, has the authority to modify configurations on behalf of the OApp owner. We strongly recommend you always make sure owner and delegate are the same address.

Setting Send and Receive Libraries

Before changing any OApp Send or Receive configurations, you should first setSendLibrary and setReceiveLibrary to the intended library. At the time of writing, the latest library for Endpoint V2 is SendULN302.sol and ReceiveULN302.sol:

import {PublicKey} from '@solana/web3.js';

import {
OftTools,
OFT_SEED,
Oft,
getSimpleMessageLibProgramId,
} from '@layerzerolabs/lz-solana-sdk-v2';

// Replace with your dstEid's and peerAddresses
const peers = [
{dstEid: 30101, peerAddress: '0x0000000000000000000000000000000000000001'},
{dstEid: 30102, peerAddress: '0x0000000000000000000000000000000000000002'},
// ...
];

const uln = new PublicKey('YOUR_ULN_ADDRESS_HERE');

// Assuming `connection` and `user` are already defined and available in the scope
for (const peer of peers) {
console.log(`Processing configurations for dstEid: ${peer.dstEid}`);

// Initialize the send library for the pathway.
const initSendLibraryTransaction = new Transaction().add(
await OftTools.createInitSendLibraryIx(user.publicKey, oftConfig, peer.dstEid),
);

const initSendLibrarySignature = await sendAndConfirmTransaction(
connection,
initSendLibraryTransaction,
[user],
);
console.log(
`✅ You initialized the send library for dstEid ${peer.dstEid}! View the transaction here: ${initSendLibrarySignature}`,
);

// Set the send library for the pathway.
const setSendLibraryTransaction = new Transaction().add(
await OftTools.createSetSendLibraryIx(user.publicKey, oftConfig, uln, peer.dstEid),
);

const setSendLibrarySignature = await sendAndConfirmTransaction(
connection,
setSendLibraryTransaction,
[user],
);
console.log(
`✅ You set the send library for dstEid ${peer.dstEid}! View the transaction here: ${setSendLibrarySignature}`,
);

// Initialize the receive library for the pathway.
const initReceiveLibraryTransaction = new Transaction().add(
await OftTools.createInitReceiveLibraryIx(user.publicKey, oftConfig, peer.dstEid),
);

const initReceiveLibrarySignature = await sendAndConfirmTransaction(
connection,
initReceiveLibraryTransaction,
[user],
);
console.log(
`✅ You initialized the receive library for dstEid ${peer.dstEid}! View the transaction here: ${initReceiveLibrarySignature}`,
);

// Set the receive library for the pathway.
const setReceiveLibraryTransaction = new Transaction().add(
await OftTools.createSetReceiveLibraryIx(user.publicKey, oftConfig, uln, peer.dstEid, 0n),
);
}
info

Why do you need to set a sendLibrary and receiveLibrary?

LayerZero uses Appendable Message Libraries. This means that while existing versions will always be immutable and available to configure, updates can still be added by deploying new Message Libraries as separate contracts and having applications manually select the new version.

If an OApp had NOT called setSendLibrary or setReceiveLibrary, the LayerZero Endpoint will fallback to the default configuration, which may be different than the MessageLib you have configured.

Explicitly setting the sendLibrary and receiveLibrary ensures that your configurations will apply to the correct library version, and will not fallback to any new library versions released.

Initialize Config

After setting your sendLibrary and receiveLibrary, you will need to initialize your config. The OFT Solana SDK provides a createInitConfigIx:

// Initialize the OFT Config for the pathway.
const initConfigTransaction = new Transaction().add(
await OftTools.createInitConfigIx(user.publicKey, oftConfig, uln, peer.dstEid),
);

const initConfigSignature = await sendAndConfirmTransaction(connection, initConfigTransaction, [
user,
]);
console.log(
`✅ You initialized the config for dstEid ${peer.dstEid}! View the transaction here: ${initConfigSignature}`,
);

After initializing the config, you can then set any of the three available config types: ExecutorConfig, SendUln, or ReceiveUln.

Setting Config

The createSetConfigIx method enforces your configuration parameters for a given configType and the remote chain's eid (endpoint ID).

// Set the Executor config for the pathway.
const setExecutorConfigTransaction = new Transaction().add(
await OftTools.createSetConfigIx(
user.publicKey,
oftConfig,
peer.dstEid,
OftTools.ConfigType, // Config Type (i.e., executor, sendUln, receiveUln)
new Uint8Array(), // config encoding
uln,
),
);

The ULN and Executor have separate config types, which change how the bytes array is structured:

CONFIG_TYPE_RECEIVE_ULN = 3; // Security Stack and block confirmation for receive config

CONFIG_TYPE_SEND_ULN = 2; // Security Stack and block confirmation for send config

CONFIG_TYPE_EXECUTOR = 1; // Executor and max message size config
info

You may be wondering why Solana has different config types available versus the EVM. Because the Solana message library has only one program account, all three config types (i.e., executor, sendUln, and receiveUln) share the same smart contract.


Based on the configType, the MessageLib will expect one of the following structures for the config bytes array.

Send Config Type Executor

See Deployed LZ Endpoints and Addresses for every chain's Executor address.

ParameterTypeDescription
maxMessageSizeuint32The maximum size of a message that can be sent cross-chain (number of bytes).
executorPubkeyThe executor implementation to pay fees to for calling the lzReceive function on the destination chain.

The example below uses the LayerZero V2 Solana SDK library to encode the arrays and call the Endpoint contract:

import {Connection, Transaction, sendAndConfirmTransaction} from '@solana/web3.js';

import {OftTools, EXECUTOR_CONFIG_SEED} from '@layerzerolabs/lz-solana-sdk-v2';

const executorConfig = UlnProgram.executorConfigBeet.serialize({
executor: PublicKey.findProgramAddressSync(
[Buffer.from(EXECUTOR_CONFIG_SEED, 'utf8')],
executor,
)[0],
maxMessageSize: 10000,
})[0];

// Set the Executor config for the pathway.
const setExecutorConfigTransaction = new Transaction().add(
await OftTools.createSetConfigIx(
user.publicKey,
oftConfig,
peer.dstEid,
OftTools.ConfigType.Executor,
executorConfig,
uln,
),
);

const setExecutorConfigSignature = await sendAndConfirmTransaction(
connection,
setExecutorConfigTransaction,
[user],
);
console.log(
`✅ Set executor configuration for dstEid ${peer.dstEid}! View the transaction here: ${setExecutorConfigSignature}`,
);

Send Config Type ULN (Security Stack)

The SendConfig describes how messages should be emitted from the source chain. See DVN Addresses for the list of available DVNs.

ParameterTypeDescription
confirmationsuint64The number of block confirmations to wait before a DVN should listen for the payloadHash. This setting can be used to ensure message finality on chains with frequent block reorganizations.
requiredDVNCountuint8The quantity of required DVNs that will be paid to send a message from the OApp.
optionalDVNCountuint8The quantity of optional DVNs that will be paid to send a message from the OApp.
optionalDVNThresholduint8The minimum number of verifications needed from optional DVNs. A message is deemed Verifiable if it receives verifications from at least the number of optional DVNs specified by the optionalDVNsThreshold, plus the required DVNs.
requiredDVNsVec<Pubkey>A vector of public keys for all required DVNs.
optionalDVNsVec<Pubkey>An vector of public keys for all optional DVNs.
caution

If you set your block confirmations too low, and a reorg occurs after your confirmation, it can materially impact your OApp or OFT.

info

You can see that the requiredDVNs and optionalDVNs are packed as a vector of PublicKeys on Solana.


The example below uses the Solana OFT SDK:

import {Connection, Transaction, sendAndConfirmTransaction} from '@solana/web3.js';

import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

const ulnConfig = UlnProgram.types.ulnConfigBeet.serialize({
confirmations: 100,
requiredDvnCount: 1,
optionalDvnCount: 1,
optionalDvnThreshold: 1,
requiredDvns: [dvn1.publicKey].sort(),
optionalDvns: [dvn3.publicKey].sort(),
})[0];

// Set send uln config for the pathway.
const setSendUlnConfigTransaction = new Transaction().add(
await OftTools.createSetConfigIx(
user.publicKey,
oftConfig,
peer.dstEid,
OftTools.ConfigType.SendUln,
ulnConfig,
uln,
),
);

const setSendConfigSignature = await sendAndConfirmTransaction(
connection,
setSendUlnConfigTransaction,
[user],
);
console.log(
`✅ Set send configuration for dstEid ${peer.dstEid}! View the transaction here: ${setSendConfigSignature}`,
);

Receive Config Type ULN (Security Stack)

The ReceiveConfig describes how to enforce and filter messages when receiving packets from the remote chain. See DVN Addresses for the list of available DVNs.

ParameterTypeDescription
confirmationsuint64The minimum number of block confirmations the DVNs must have waited for their verification to be considered valid.
requiredDVNCountuint8The quantity of required DVNs that must verify before receiving the OApp's message.
optionalDVNCountuint8The quantity of optional DVNs that must verify before receiving the OApp's message.
optionalDVNThresholduint8The minimum number of verifications needed from optional DVNs. A message is deemed Verifiable if it receives verifications from at least the number of optional DVNs specified by the optionalDVNsThreshold, plus the required DVNs.
requiredDVNsVec<Pubkey>A vector of PublicKeys for all required DVNs to receive verifications from.
optionalDVNsVec<Pubkey>An vector of PublicKeys for all optional DVNs to receive verifications from.
caution

If you set your block confirmations too low, and a reorg occurs after your confirmation, it can materially impact your OApp or OFT.


Use the ULN config type and the struct definition to form your configuration for the call:

import {Connection, Transaction, sendAndConfirmTransaction} from '@solana/web3.js';

import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

const ulnConfig = UlnProgram.types.ulnConfigBeet.serialize({
confirmations: 100,
requiredDvnCount: 1,
optionalDvnCount: 1,
optionalDvnThreshold: 1,
requiredDvns: [dvn1.publicKey].sort(),
optionalDvns: [dvn3.publicKey].sort(),
})[0];

// Set the receive uln config for the pathway.
const setReceiveUlnConfigTransaction = new Transaction().add(
await OftTools.createSetConfigIx(
user.publicKey,
oftConfig,
peer.dstEid,
OftTools.ConfigType.ReceiveUln,
ulnConfig,
uln,
),
);

const setReceiveConfigSignature = await sendAndConfirmTransaction(
connection,
setReceiveUlnConfigTransaction,
[user],
);
console.log(
`✅ Set receive configuration for dstEid ${peer.dstEid}! View the transaction here: ${setReceiveConfigSignature}`,
);

Debugging Configurations

A correct OApp configuration example:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1_Address_A, DVN2_Address_A)requiredDVNs: Array(DVN1_Address_B, DVN2_Address_B)
tip

The sending OApp's SendLibConfig (OApp on Chain A) and the receiving OApp's ReceiveLibConfig (OApp on Chain B) match!

Block Confirmation Mismatch

An example of an incorrect OApp configuration:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 5confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1, DVN2)requiredDVNs: Array(DVN1, DVN2)
danger

The above configuration has a block confirmation mismatch. The sending OApp (Chain A) will only wait 5 block confirmations, but the receiving OApp (Chain B) will not accept any message with less than 15 block confirmations.

Messages will be blocked until either the sending OApp has increased the outbound block confirmations, or the receiving OApp decreases the inbound block confirmation threshold.

DVN Mismatch

Another example of an incorrect OApp configuration:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 1requiredDVNCount: 2
requiredDVNs: Array(DVN1)requiredDVNs: Array(DVN1, DVN2)
danger

The above configuration has a DVN mismatch. The sending OApp (Chain A) only pays DVN 1 to listen and verify the packet, but the receiving OApp (Chain B) requires both DVN 1 and DVN 2 to mark the packet as verified.

Messages will be blocked until either the sending OApp has added DVN 2's address on Chain A to the SendUlnConfig, or the receiving OApp removes DVN 2's address on Chain B from the ReceiveUlnConfig.

Dead DVN

This configuration includes a Dead DVN:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1, DVN2)requiredDVNs: Array(DVN1, DVN_DEAD)
danger

The above configuration has a Dead DVN. Similar to a DVN Mismatch, the sending OApp (Chain A) pays DVN 1 and DVN 2 to listen and verify the packet, but the receiving OApp (Chain B) has currently set DVN 1 and a Dead DVN to mark the packet as verified.

Since a Dead DVN for all practical purposes should be considered a null address, no verification will ever match the dead address.

Messages will be blocked until the receiving OApp removes or replaces the Dead DVN from the ReceiveUlnConfig.