Skip to main content
Version: Endpoint V2 Docs

Aptos DVN and Executor Configuration

Before setting your DVN and Executor Configuration, you should review the Security Stack Core Concepts.

You can manually configure your Aptos Move OApp’s Send and Receive settings by:

  • Reading Defaults: Use the get_config method to see default configurations.

  • Setting Libraries: Call set_send_library and set_receive_library to choose the correct Message Library version.

  • Setting Configs: Use the set_config instruction to update your custom DVN and Executor settings.

For both Send and Receive configurations, make sure that for a given channel:

  • Send (Chain A) settings match the Receive (Chain B) settings.

  • DVN addresses are provided in alphabetical order.

  • Block confirmations are correctly set to avoid mismatches.

Use the LayerZero CLI

The LayerZero CLI has abstracted these calls for every supported chain. See the CLI Setup Guide to easily deploy, configure, and send messages using LayerZero.

Setting Send / Receive Libraries

In Aptos, you call the endpoint_v2::endpoint module’s entry or friend functions to pick the library you want for sending or receiving messages.

A typical library in current Aptos V2 is the ULN 302 library (uln_302::msglib). If you do not call set_send_library or set_receive_library, your OApp falls back to the default library for that remote EID.

Note: The Endpoint has built-in constraints:

  1. dst_eid in set_send_library(...) must be valid for that library.

  2. src_eid in set_receive_library(...) must be valid for that library (i.e., the library says it supports receiving from that chain).

When you set a new library, the old library is replaced. You can optionally specify a grace_period on the receive side so the old library can continue verifying messages for a set time. This is how you “roll over” from one library version to another.

Typescript

Below is an example of how you might call the endpoint_v2::endpoint::set_send_library or endpoint_v2::endpoint::set_receive_library function using the Aptos JS SDK.

import {
Account,
Aptos,
Ed25519PrivateKey,
PrivateKey,
PrivateKeyVariants,
SimpleTransaction,
InputEntryFunctionData,
AptosConfig,
} from '@aptos-labs/ts-sdk';

const NODE_URL = 'https://fullnode.testnet.aptoslabs.com/v1';

// Replace with your actual private key or create from a local mnemonic
const ADMIN_PRIVATE_KEY_HEX = '0x...';
const ADMIN_ACCOUNT_ADDRESS = '0x...';

// OApp data
const OAPP_ADDRESS = '0xMyOApp'; // your OApp’s address on Aptos
const REMOTE_EID = 30101; // e.g. the remote chain’s EID
const MSGLIB_ADDRESS = '0xULN302'; // The “Send” library you want
const NETWORK = 'testnet'; // "testnet" or "mainnet"

// Create the private key
const aptos_private_key = PrivateKey.formatPrivateKey(
ADMIN_PRIVATE_KEY_HEX,
PrivateKeyVariants.Ed25519,
);

// Create the signer account
const signer_account = Account.fromPrivateKey({
privateKey: new Ed25519PrivateKey(aptos_private_key),
address: ADMIN_ACCOUNT_ADDRESS,
});

// Create the Aptos client
const aptos = new Aptos(new AptosConfig({network: NETWORK}));

The function signature in your OApp might look like:

public entry fun set_send_library(
account: &signer,
remote_eid: u32,
msglib: address,
) { ... }

Which can be invoked like:

async function setSendLibrary() {
// 1. Build the transaction payload and transaction
const payload: InputEntryFunctionData = {
function: `${OAPP_ADDRESS}::oapp_core::set_send_library`,
functionArguments: [REMOTE_EID, MSGLIB_ADDRESS],
};

const transaction: SimpleTransaction = await aptos.transaction.build.simple({
sender: ADMIN_ACCOUNT_ADDRESS,
data: payload,
options: {
maxGasAmount: 30000,
},
});

// 2. Generate and sign transaction
const signedTransaction = await aptos.signAndSubmitTransaction({
signer: signer_account,
transaction: transaction,
});

// 3. Wait for confirmation
const executedTransaction = await aptos.waitForTransaction({
transactionHash: signedTransaction.hash,
});

console.log('set_send_library transaction completed:', executedTransaction.hash);
}

setSendLibrary()
.then(() => {
console.log('Done setting send library');
})
.catch(console.error);

Setting Security & Executor Configuration

A similar approach to EVM’s setConfig is available in Aptos. You can call:

public entry fun set_config(
account: &signer,
msglib: address,
eid: u32,
config_type: u32,
config: vector<u8>,
) {
assert_authorized(address_of(account));
endpoint::set_config(&oapp_store::call_ref(), msglib, eid, config_type, config);
}
  • msglib is the library you are configuring (e.g. @uln_302).

  • eid is the remote endpoint ID you are targeting (e.g. 30101 if referencing “Chain B’s ID”).

  • config_type is typically 1 for Executor and 2 or 3 for ULN-based “send” or “receive” config.

  • config is a serialized bytes array containing your DVN addresses, confirmations, or max message size, etc.

Typical ULN & Executor Structures

The uln_302::configuration module references these data structures:

ULN Config (Security Stack)

struct UlnConfig has copy, drop {
confirmations: u64,
optional_dvn_threshold: u8,
required_dvns: vector<address>,
optional_dvns: vector<address>,
use_default_for_confirmations: bool,
use_default_for_required_dvns: bool,
use_default_for_optional_dvns: bool,
}
  • confirmations: how many blocks to wait on the source chain for finality.

  • required_dvns: the DVNs that must sign your message.

  • optional_dvns: the DVNs that may sign your message if they reach the threshold.

  • optional_dvn_threshold: how many optional DVNs are needed if you have optional DVNs.

  • use_default_for_*: determines if we fallback to a default config for certain fields.

In EVM you’d see fields like requiredDVNCount, requiredDVNs, optionalDVNCount, etc. In Aptos, it’s stored as a single struct with arrays for addresses.

Executor Config

struct ExecutorConfig has copy, drop {
max_message_size: u32,
executor_address: address,
}
  • max_message_size: max size of cross-chain messages, in bytes.

  • executor_address: which executor is authorized/paid to lz_receive your message.

Distinction vs. EVM

Where EVM calls setConfigParam[], on Aptos, we pass a single (config_type, config) each time. If you want to set both Executor and ULN in one go, call set_config with each config type. Some developers write a convenience function to do both in a single transaction.

The uln_302::configuration module handles the actual decode:

  • CONFIG_TYPE_EXECUTOR = 1
  • CONFIG_TYPE_SEND_ULN = 2
  • CONFIG_TYPE_RECV_ULN = 3

It extracts your config bytes, e.g. extract_uln_config for a ULN struct or extract_executor_config for an executor struct.

Example: Setting a “send side” ULN config might look like:

async function setUlnConfig(sendLibrary: string, remoteEid: number, serializedConfig: Uint8Array) {
// config_type = 2 for "send side" or 3 for "receive side"
const CONFIG_TYPE_SEND_ULN = 2;

// Suppose your OApp entry function is:
// public entry fun set_config(account: &signer, msglib: address, eid: u32, config_type: u32, config: vector<u8>)
const payload: InputEntryFunctionData = {
function: `${OAPP_ADDRESS}::oapp_core::set_config`,
functionArguments: [sendLibrary, remoteEid, CONFIG_TYPE_SEND_ULN, serializedConfig],
};

const rawTransaction = await aptos.transaction.build.simple({
sender: ADMIN_ACCOUNT_ADDRESS,
data: payload,
options: {
maxGasAmount: 30000,
},
});

const signedTransaction = await aptos.signAndSubmitTransaction({
signer: signer_account,
transaction: rawTransaction,
});

const executedTransaction = await aptos.waitForTransaction({
transactionHash: signedTransaction.hash,
});

console.log(`set_config ULN success: ${signedTransaction.hash}`);
}

The Executor config is CONFIG_TYPE_EXECUTOR = 1. You pass a serialized (max_message_size, executor_address) structure.

async function setExecutorConfig(
sendLibrary: string,
remoteEid: number,
execConfigBytes: Uint8Array,
) {
// config_type = 1 for "executor"
const CONFIG_TYPE_EXECUTOR = 1;

const payload: InputEntryFunctionData = {
function: `${OAPP_ADDRESS}::oapp_core::set_config`,
functionArguments: [sendLibrary, remoteEid, CONFIG_TYPE_EXECUTOR, execConfigBytes],
};

const rawTransaction = await aptos.transaction.build.simple({
sender: ADMIN_ACCOUNT_ADDRESS,
data: payload,
options: {
maxGasAmount: 30000,
},
});

const signedTransaction = await aptos.signAndSubmitTransaction({
signer: signer_account,
transaction: rawTransaction,
});

const executedTransaction = await aptos.waitForTransaction({
transactionHash: signedTransaction.hash,
});

console.log(`set_config Executor success: ${signedTransaction.hash}`);
}

Resetting to Default

If you pass a config that sets fields like confirmations = 0, required_dvns = [], and sets use_default_for_confirmations = true, then the OApp will fallback to whatever the default is on that chain.

Similarly, if you pass an ExecutorConfig with max_message_size = 0 and executor_address = @0x0, you revert to default. The uln_302::configuration module merges your OApp’s config with the chain’s default config if you set use_default_for_* = true.

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.

Key Functions in endpoint_v2::endpoint

Below are the main wiring functions used for configuration.

They typically are invoked in your OApp’s admin or delegate entry function.

  • register_receive_pathway(call_ref, src_eid, sender_bytes32): Inform the endpoint that you accept messages from (src_eid, sender).

  • set_send_library(call_ref, remote_eid, msglib): Tells the endpoint which library to use for sending messages to remote_eid.

  • set_receive_library(call_ref, remote_eid, msglib, grace_period): Tells the endpoint which library to use for receiving messages from remote_eid. Optionally specify a grace_period in blocks.

  • set_config(call_ref, msglib, eid, config_type, config_bytes): Instruct the chosen library to store or merge your OApp’s custom config for that EID.

Conclusion

The Aptos V2 Endpoint wiring parallels the approach on EVM:

  • Choose your libraries for sending and receiving (set_send_library, set_receive_library).

  • Set your ULN or Executor configs via set_config on the chosen library’s address, specifying the remote EID.

  • Ensure your sending chain’s config aligns with the receiving chain’s config (DVNs, block confirmations, etc.), or your messages may be blocked .

  • If you want to revert to defaults, pass a config that indicates use_default_for_* = true or sets addresses to @0x0.

By following these steps, you can precisely control the LayerZero V2 security stack (DVNs), block confirmations, and executor settings on Aptos—just as you would with the EVM-based setSendLibrary, setReceiveLibrary, and setConfig flow.