L1 and Core
The L1 bridge ZkLighter intermediates messaging between L1 and Core.
Arbitrary cross-domain messaging is not supported. The protocol defines a fixed set of messages that can be sent between L1 and Core.
L1 to Core
L1 accounts can request actions on Core, such as placing orders or transferring tokens. Each request is submitted to a priority queue on L1 and processed asynchronously by the Core sequencer in first-in, first-out order.
Two L1 contracts provide access to these requests.
L1CoreMessagingexposes all supported L1-to-Core writes. Some methods are restricted toL1InteropManageror governance.L1InteropManagerhandles token-specific logic and permissions before calling the restrictedL1CoreMessagingmethods. See Token transfers and Token linking for those flows.
Every request ultimately passes through ZkLighter.addPriorityRequestExt(...). This is the bridge's general mechanism for adding Core priority requests, but it accepts calls only from L1CoreMessaging. Thus, users can only submit supported operations to Core rather than engaging in arbitrary messaging.
L1-controlled Core accounts can use this priority queue to bypass censorship of transactions submitted directly to Core. The Core sequencer is obligated to process these requests. If the oldest pending request remains unfulfilled past its 14-day expiration, anyone can activate the escape hatch, allowing L1-controlled accounts to exit their balances on L1.
For EVM-controlled Core accounts, circumvention of sequencer downtime or censorship is covered in L1 and EVM message passing.
Submitting a priority request to Core
Submit a request to Core through the appropriate L1 contract introduced above. To authorize operations on an existing account, L1CoreMessaging resolves the caller's L1-controlled master account and includes its index in the request. The same address on Lighter EVM does not authorize an L1-controlled account, or vice versa. See Accounts.
If the L1 call reverts, no request is submitted. A successful L1 transaction confirms only submission, not successful execution on Core. Since submission and fulfillment are not atomic, a failure on Core does not revert the initiating L1 transaction.
GET /api/v1/txFromL1TxHash?hash=... returns the Core transaction for an L1 transaction hash. Core includes every priority request, so status is 3 once Core has processed the request, whether or not the operation succeeded. The outcome is the ae field of event_info: both are JSON strings, and ae is empty on success. On failure it holds a Core error code:
{
"status": 3,
"event_info": "{\"a\":4,\"f\":0,\"ti\":0,\"ae\":\"{\\\"code\\\":21128,\\\"message\\\":\\\"invalid master account index\\\"}\"}"
}L1CoreMessaging does not check that the caller controls the target accountIndex; Core does. The request above targeted an account under another master account, so L1 accepted it and Core rejected it with 21128.
If Core processing produces an outgoing message, track that delivery separately. For example, L1CoreMessaging.sendToEvm(...) requests a debit on Core, which then schedules a transfer to EVM. See L1-initiated send to EVM for the full flow.
Supported requests
Users can directly call the following methods on L1CoreMessaging to submit account operations.
| Method | Requested Core operation |
|---|---|
changePubKey(...) | Register or replace an API key. |
createOrder(...) | Reduce or close a perps position: Core runs it as a reduce-only, immediate-or-cancel order. |
cancelAllOrders(...) | Cancel the account's orders. |
burnShares(...) | Redeem public-pool shares. |
withdraw(...) | Initiate a withdrawal to L1. |
withdrawToL1Address(...) | Initiate a withdrawal to a specified L1 recipient. |
sendToEvm(...) | Initiate a transfer from Core to EVM. |
The following methods accept calls only from L1InteropManager.
| Method | Requested Core operation |
|---|---|
registerDeposit(...) | Credit an L1 deposit. |
registerL1NativeAssetOnCore(...), registerEvmNativeAssetOnCore(...), registerCoreNativeAssetOnCore(...) | Register an asset on Core. See Registering on Core for L1-native and EVM-native assets. |
finalizeEvmWrapperLink(...) | Record the EVM wrapper for an L1-native asset. |
finalizeL1WrapperLink(...) | Record the L1 wrapper for an EVM-native asset. |
L1CoreMessaging also exposes setSystemConfig(...), updateAsset(...), createMarket(...), and updateMarket(...) for governance-authorized callers. These submit protocol-configuration requests through the same queue.
Core to L1
Core emits L1 Calls in the following cases.
- Processing withdrawals to L1.
- Returning unaccepted L1 deposit amounts.
- Confirming an EVM-native token link.
These L1 Calls are included in a batch. Once that batch has been executed on L1, anyone can call the bridge to execute them.
For Core-originated L1 Calls, l2Sender is always the Core system address.
Executing Core messages on L1
Once the batch containing the call has been executed on L1, retrieve the call data from Core using GET /api/v1/coreL1Call?tx_hash=.... Set CORE_API_URL to your network's Core API URL and CORE_TX_HASH to the hash of the Core transaction that generated the call.
For operations initiated on L1 or EVM, obtain CORE_TX_HASH as follows.
- From L1. Look up the initiating L1 transaction with
GET /api/v1/txFromL1TxHash?hash=...and use the returned Core transaction'shashfield. - From EVM. Derive the Core transaction hash from the request's
WriteToCoreevent.
curl --get "$CORE_API_URL/api/v1/coreL1Call" \
--data-urlencode "tx_hash=$CORE_TX_HASH" | jq{
"code": 200,
"call": {
"l2_sender": "0x7fffffffffffffffffffffffffffffffffffffff",
"target": "0x...",
"value": "0",
"msg_nonce": "42",
"calldata": "0x...",
"l1_call_hash": "0x..."
}
}The response's call object contains the arguments needed to execute the L1 Call through ZkLighter.executeCoreL1Call(...). Submit them once the batch has been executed on L1.
Call data may be available before batch execution. The API does not report whether the L1 Call has already been executed.
function executeCoreL1Call(
address _l2Sender,
address to,
uint256 value,
uint256 msgNonce,
bytes calldata data
) external;The bridge checks that the call belongs to an executed batch and has not already been executed.
During execution, the target can read the authenticated cross-domain sender through the bridge's l2Sender() function. To authenticate a Core-originated L1 Call, the target MUST check the following.
msg.senderis the address of theZkLighterbridge.ZkLighter.l2Sender()equals the Core system address.
If executeCoreL1Call(...) fails, it can be retried. Once it succeeds, the same L1 Call cannot be executed again. The transaction sender pays the L1 gas fees but cannot alter the message itself.
Updated about 7 hours ago
