Security Stack & Executor Configuration (Aptos V2)
Every LayerZero V2 Endpoint can send and receive cross-chain messages. Because of this, each Endpoint has a separate Send and Receive configuration that an OApp can customize per target Endpoint (EID)—which we call a pathway.
A “pathway” is a pair (OApp1 on Chain A, OApp2 on Chain B) that have each called set_peer
in their OApp modules to enable cross-chain messaging.
For the cross-chain configuration to be correct:
The Send Library config on Chain A should be consistent with the Receive Library config on Chain B.
The Receive Library config on Chain A should be consistent with the Send Library config on Chain B.
Hence, each chain sets Send and Receive libraries and configuration parameters to ensure the DVNs (Decentralized Verifier Networks), block confirmations, and executors match on both sides.
Default vs. Custom Configuration
When a chain is newly integrated, LayerZero typically provides a default security + executor configuration for each EID. This default exists in the msglib_manager
or uln_302::configuration
modules. If you do nothing, your OApp’s sends/receives might fallback to the default “ULN 302” library with its preconfigured DVNs and block confirmations.
Default means: The chain’s
default_send_library
ordefault_receive_library
(and associated ULN settings) are used if you have not calledsetSendLibrary
/setReceiveLibrary
orset_config
.If you want custom parameters (like custom DVNs, or a different number of block confirmations), you call the Endpoint’s
set_send_library
,set_receive_library
, andset_config
functions with your own values.
If the default includes a “Dead DVN” or something you don’t want, you’ll want to override that or provide your own working DVNs.
Defaults should be though of as placeholder configurations, and will not always remain constant. Therefore, it is strongly encouraged that you set a custom configuration, even if that configuration is identical to today's default settings.
This will enforce that these settings remain unchanged, until the OApp's selected Delegate changes them again.
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:
dst_eid
inset_send_library(...)
must be valid for that library.src_eid
inset_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 tolz_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: 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.
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 toremote_eid
.set_receive_library(call_ref, remote_eid, msglib, grace_period)
: Tells the endpoint which library to use for receiving messages fromremote_eid
. Optionally specify agrace_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.