DVN and Executor Configuration on Starknet
This guide explains how to configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for your Starknet OApp or OFT using the SDK.
Overview
Configuration is done through the SDK provided by @layerzerolabs/lz-v2-protocol-starknet. All examples use SDK methods to interact with the Endpoint and message library contracts.
Configuration flow:
- Set delegate (required if configuring via external account)
- Set message libraries (optional)
- Configure DVNs for send/receive (optional but recommended)
- Set enforced options (optional)
- Set peer addresses (required - opens pathway, call last!)
Always configure in this order. Setting peers last ensures the pathway isn't opened until all security settings are in place.
SDK Setup
import {RpcProvider, Account} from 'starknet';
import {
getEndpointV2Contract,
getOAppContract,
getUltraLightNodeContractWithAddress,
encodeUlnConfig,
encodeExecutorConfig,
MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';
// Setup provider and account
const provider = new RpcProvider({nodeUrl: 'YOUR_RPC_URL'});
const account = new Account({provider, address: accountAddress, signer: privateKey});
// Get contract instances
const oappContract = await getOAppContract(oappAddress, provider);
const endpointContract = await getEndpointV2Contract(
ChainName.STARKNET,
Environment.TESTNET,
provider,
);
// If you already have the Endpoint address, you can use:
// const endpointContract = await getEndpointV2ContractWithAddress(ENDPOINT_ADDRESS, provider);
// This is recommended when running outside the monorepo.
Prerequisite: Set Delegate
Endpoint configuration calls (set_send_library, set_receive_library, set_send_configs, set_receive_configs) require the caller to be the OApp itself or an authorized delegate. If you're configuring from an external account, set a delegate first (owner-only):
const setDelegateCall = oappContract.populateTransaction.set_delegate(accountAddress);
await account.execute([setDelegateCall]);
Use the address of the account that will submit the endpoint configuration transactions.
Default Configuration
LayerZero provides sensible defaults. If you don't configure custom settings:
- DVN: LayerZero Labs DVN
- Executor: LayerZero Labs Executor
- Send Library: ULN302
- Receive Library: ULN302
You can query the default configuration via the Endpoint or check LayerZero Deployments.
Configuration Methods
Set Peer
import {getOAppContract} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId} from '@layerzerolabs/lz-definitions';
const oapp = await getOAppContract(oappAddress, provider);
// Peer address as Bytes32 (left-padded for EVM addresses)
const peerBytes32 = {value: BigInt('0x000000000000000000000000' + evmAddress.slice(2))};
const call = oapp.populateTransaction.set_peer(
EndpointId.ETHEREUM_V2_MAINNET, // Remote chain endpoint ID
peerBytes32,
);
await account.execute([call]);
Address format: Use Bytes32 for all peers. EVM addresses (20 bytes) must be left-padded with zeros to 32 bytes.
Set Message Libraries
import {getEndpointV2Contract} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';
const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
// Set custom send library
const setSendLibCall = endpoint.populateTransaction.set_send_library(
oappAddress,
EndpointId.ETHEREUM_V2_MAINNET,
messageLibAddress,
);
// Set custom receive library (with grace period)
const setReceiveLibCall = endpoint.populateTransaction.set_receive_library(
oappAddress,
EndpointId.ETHEREUM_V2_MAINNET,
messageLibAddress,
0, // Grace period in blocks (0 = immediate)
);
await account.execute([setSendLibCall, setReceiveLibCall]);
Default: Uses Endpoint defaults if not configured. If you see DEFAULT_SEND_LIB_UNAVAILABLE or UNSUPPORTED_EID, set the send/receive libraries explicitly and ensure the EID is supported by the ULN.
import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet';
const uln = await getUltraLightNodeContractWithAddress(ulnAddress, provider);
const canSend = await uln.is_supported_send_eid(remoteEid);
const canReceive = await uln.is_supported_receive_eid(remoteEid);
DVN Configuration
Configure Send DVN (Outbound)
import {
getEndpointV2Contract,
encodeUlnConfig,
MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';
const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
const remoteEid = EndpointId.ETHEREUM_V2_MAINNET;
// Get the current send library address
const sendLibResponse = await endpoint.get_send_library(oappAddress, remoteEid);
const sendLibAddress = sendLibResponse.lib;
// Encode ULN configuration
const ulnConfig = encodeUlnConfig({
confirmations: 15,
has_confirmations: true,
required_dvns: [LAYERZERO_DVN_ADDRESS, PARTNER_DVN_ADDRESS], // Must be sorted ascending
has_required_dvns: true,
optional_dvns: [],
optional_dvn_threshold: 0,
has_optional_dvns: false,
});
// Set send config
const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [
{
eid: remoteEid,
config_type: MessageLibConfigType.ULN, // 2
config: ulnConfig,
},
]);
await account.execute([call]);
Configure Receive DVN (Inbound)
// Get the current receive library address
const receiveLibResponse = await endpoint.get_receive_library(oappAddress, remoteEid);
const receiveLibAddress = receiveLibResponse.lib;
// Encode ULN configuration for receive
const ulnConfig = encodeUlnConfig({
confirmations: 15,
has_confirmations: true,
required_dvns: [LAYERZERO_DVN_ADDRESS],
has_required_dvns: true,
optional_dvns: [],
optional_dvn_threshold: 0,
has_optional_dvns: false,
});
// Set receive config
const call = endpoint.populateTransaction.set_receive_configs(oappAddress, receiveLibAddress, [
{
eid: remoteEid,
config_type: MessageLibConfigType.ULN, // 2
config: ulnConfig,
},
]);
await account.execute([call]);
ULN Configuration Structure
| Field | Type | Description |
|---|---|---|
confirmations | number | Block confirmations required before verification |
has_confirmations | boolean | Set true to use custom value |
required_dvns | string[] | DVN addresses (all must verify) - sorted ascending |
has_required_dvns | boolean | Set true to use custom DVNs |
optional_dvns | string[] | Optional DVN addresses - sorted ascending |
optional_dvn_threshold | number | How many optional DVNs must verify |
has_optional_dvns | boolean | Set true to use custom optional DVNs |
The has*\*fields indicate whether the corresponding value should override the default configuration. Set them totrue when you want to use custom values, otherwise the protocol defaults will be used.
DVN addresses in required_dvns and optional_dvns must be sorted in ascending order. The contract will revert if unsorted or duplicate DVNs are provided.
Config types: MessageLibConfigType.EXECUTOR = 1, MessageLibConfigType.ULN = 2
Configuring Executor
The Executor delivers messages on the destination chain.
import {
getEndpointV2Contract,
encodeExecutorConfig,
MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {ChainName, Environment} from '@layerzerolabs/lz-definitions';
const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
// Encode executor configuration
const executorConfig = encodeExecutorConfig({
max_message_size: 10000,
executor: EXECUTOR_ADDRESS,
});
// Set executor config (only for send direction)
const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [
{
eid: remoteEid,
config_type: MessageLibConfigType.EXECUTOR, // 1
config: executorConfig,
},
]);
await account.execute([call]);
Executor Config Structure
| Field | Type | Description |
|---|---|---|
max_message_size | number | Maximum message size in bytes |
executor | string | Executor contract address |
Setting Enforced Options
Enforced options set minimum execution parameters that users cannot override.
This requires the OAppOptionsType3 component (included in OFT contracts). If your ABI doesn't expose set_enforced_options, load your compiled contract artifact and call the entrypoint directly.
import {Contract} from 'starknet';
import {Options} from '@layerzerolabs/lz-v2-utilities';
import compiledArtifact from './path/to/contract_class.json';
// Use your compiled ABI for OFT/OAppOptionsType3 since getOAppContract
// only exposes the base OApp interface.
const oappAbi = compiledArtifact.abi; // Load from target/dev/*.contract_class.json
const oapp = new Contract({abi: oappAbi, address: oappAddress, provider}).typedv2(oappAbi);
// Build options with minimum gas requirements
const options = Options.newOptions()
.addExecutorLzReceiveOption(200000, 0) // 200k gas for lz_receive
.toBytes();
// Set enforced options for SEND message type (1)
const call = oapp.populateTransaction.set_enforced_options([
{
eid: remoteEid,
msg_type: 1,
options,
},
]);
await account.execute([call]);
If you prefer sncast, call the entrypoint directly:
# lzReceive gas = 120000, value = 0 (no compose)
sncast invoke \
--contract-address <OFT_ADDRESS> \
--function set_enforced_options \
--network sepolia \
--arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: <DST_EID>, msg_type: 1, options: core::byte_array::ByteArray { data: array![], pending_word: 0x0003010011010000000000000000000000000001d4c0, pending_word_len: 22 } }]'
Message Types
| Type | Value | Description |
|---|---|---|
SEND | 1 | Standard OFT transfer |
Complete Configuration Example
import {RpcProvider, Account, Contract} from 'starknet';
import {
getEndpointV2Contract,
getOAppContract,
encodeUlnConfig,
encodeExecutorConfig,
MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {Options} from '@layerzerolabs/lz-v2-utilities';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';
import compiledArtifact from './path/to/contract_class.json';
async function configureOApp() {
const provider = new RpcProvider({nodeUrl: RPC_URL});
const account = new Account({provider, address: ACCOUNT_ADDRESS, signer: PRIVATE_KEY});
const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
const oapp = await getOAppContract(OFT_ADDRESS, provider);
const oappOptions = new Contract({
abi: compiledArtifact.abi,
address: OFT_ADDRESS,
provider,
}).typedv2(compiledArtifact.abi);
const remoteEid = EndpointId.ETHEREUM_V2_MAINNET;
const ULN_ADDRESS = '0x...'; // ULN302 send library address
// Authorize the account to configure endpoint settings (owner-only)
const setDelegateCall = oapp.populateTransaction.set_delegate(ACCOUNT_ADDRESS);
// Set libraries (required if defaults aren't configured for the EID)
const setSendLibCall = endpoint.populateTransaction.set_send_library(
OFT_ADDRESS,
remoteEid,
ULN_ADDRESS,
);
const setReceiveLibCall = endpoint.populateTransaction.set_receive_library(
OFT_ADDRESS,
remoteEid,
ULN_ADDRESS,
0,
);
// 1. Configure send DVN + Executor
const sendConfigCall = endpoint.populateTransaction.set_send_configs(OFT_ADDRESS, ULN_ADDRESS, [
{
eid: remoteEid,
config_type: MessageLibConfigType.ULN,
config: encodeUlnConfig({
confirmations: 15,
has_confirmations: true,
required_dvns: [LAYERZERO_DVN, PARTNER_DVN],
has_required_dvns: true,
optional_dvns: [],
optional_dvn_threshold: 0,
has_optional_dvns: false,
}),
},
{
eid: remoteEid,
config_type: MessageLibConfigType.EXECUTOR,
config: encodeExecutorConfig({
max_message_size: 10000,
executor: EXECUTOR_ADDRESS,
}),
},
]);
// 2. Configure receive DVN
const receiveConfigCall = endpoint.populateTransaction.set_receive_configs(
OFT_ADDRESS,
ULN_ADDRESS,
[
{
eid: remoteEid,
config_type: MessageLibConfigType.ULN,
config: encodeUlnConfig({
confirmations: 15,
has_confirmations: true,
required_dvns: [LAYERZERO_DVN],
has_required_dvns: true,
optional_dvns: [],
optional_dvn_threshold: 0,
has_optional_dvns: false,
}),
},
],
);
// 3. Set enforced options
const enforcedOptionsCall = oappOptions.populateTransaction.set_enforced_options([
{
eid: remoteEid,
msg_type: 1, // SEND message type
options: Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(),
},
]);
// 4. Set peer (LAST!)
const setPeerCall = oapp.populateTransaction.set_peer(remoteEid, {
value: BigInt('0x000000000000000000000000' + EVM_OFT_ADDRESS.slice(2)),
});
// Execute all in atomic transaction
await account.execute([
setDelegateCall,
setSendLibCall,
setReceiveLibCall,
sendConfigCall,
receiveConfigCall,
enforcedOptionsCall,
setPeerCall,
]);
console.log('Configuration complete!');
}
Reading Configuration
Get Current Send Config
import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet';
const ulnContract = await getUltraLightNodeContractWithAddress(sendLibAddress, provider);
// Get executor config
const executorConfig = await ulnContract.get_raw_oapp_executor_config(oappAddress, remoteEid);
console.log('Max message size:', executorConfig.max_message_size);
console.log('Executor:', executorConfig.executor);
// Get ULN config
const ulnConfig = await ulnContract.get_raw_oapp_uln_send_config(oappAddress, remoteEid);
console.log('Confirmations:', ulnConfig.confirmations);
console.log('Required DVNs:', ulnConfig.required_dvns);
Get Current Receive Config
const ulnConfig = await ulnContract.get_raw_oapp_uln_receive_config(oappAddress, remoteEid);
console.log('Confirmations:', ulnConfig.confirmations);
console.log('Required DVNs:', ulnConfig.required_dvns);
Get Peer
const oapp = await getOAppContract(oappAddress, provider);
const peer = await oapp.get_peer(remoteEid);
console.log('Peer:', peer.value.toString(16));
Gas Recommendations
| Operation | Recommended Gas | Notes |
|---|---|---|
lz_receive (OApp) | 200,000 | Basic message handling |
lz_receive (OFT) | 200,000 | Token credit operation |
Always test your specific use case on testnet to determine accurate gas requirements.
Next Steps
- Protocol Overview - Message lifecycle
- Technical Reference - Deployment guide
- Troubleshooting - Configuration errors