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).
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.
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 Stack | Executor | |
---|---|---|
Default Send and Receive A | requiredDVNs: [ Google Cloud, LayerZero Labs ] | LayerZero Labs |
Default Send and Receive B | requiredDVNs: [ Polyhedra, LayerZero Labs ] | LayerZero Labs |
Default Send and Receive C | requiredDVNs: [ Dead DVN, LayerZero Labs ] | LayerZero Labs |
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,
);
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>
}
]
}
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.
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),
);
}
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
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.
Parameter | Type | Description |
---|---|---|
maxMessageSize | uint32 | The maximum size of a message that can be sent cross-chain (number of bytes). |
executor | Pubkey | The 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.
Parameter | Type | Description |
---|---|---|
confirmations | uint64 | The 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. |
requiredDVNCount | uint8 | The quantity of required DVNs that will be paid to send a message from the OApp. |
optionalDVNCount | uint8 | The quantity of optional DVNs that will be paid to send a message from the OApp. |
optionalDVNThreshold | uint8 | The 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. |
requiredDVNs | Vec<Pubkey> | A vector of public keys for all required DVNs. |
optionalDVNs | Vec<Pubkey> | An vector of public keys for all optional DVNs. |
If you set your block confirmations too low, and a reorg occurs after your confirmation, it can materially impact your OApp or OFT.
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.
Parameter | Type | Description |
---|---|---|
confirmations | uint64 | The minimum number of block confirmations the DVNs must have waited for their verification to be considered valid. |
requiredDVNCount | uint8 | The quantity of required DVNs that must verify before receiving the OApp's message. |
optionalDVNCount | uint8 | The quantity of optional DVNs that must verify before receiving the OApp's message. |
optionalDVNThreshold | uint8 | The 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. |
requiredDVNs | Vec<Pubkey> | A vector of PublicKeys for all required DVNs to receive verifications from. |
optionalDVNs | Vec<Pubkey> | An vector of PublicKeys for all optional DVNs to receive verifications from. |
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1_Address_A, DVN2_Address_A) | requiredDVNs: Array(DVN1_Address_B, DVN2_Address_B) |
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: 5 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN2) |
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 1 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1) | requiredDVNs: Array(DVN1, DVN2) |
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN_DEAD) |
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.