linera-io/linera-protocol · error · EvmExecutionError
It is illegal to call function instantiate from an operation
Error message
It is illegal to call function instantiate from an operation
What it means
Linera's EVM integration reserves a set of base-contract entry points (execute_message, process_streams, summarize_events, instantiate) for system-initiated calls only. Whenever an EVM application processes a user operation or an instantiation transaction, forbid_execute_operation_origin compares the first 4 bytes of the input against those reserved ABI selectors. An operation whose calldata starts with the instantiate(bytes) selector ([156,163,60,158]) is rejected with IllegalOperationCall, because instantiate is only reachable through the shared-contract creation flow.
Source
Thrown at linera-execution/src/evm/inputs.rs:181
/// This is the selector of `instantiate` that should be called
/// only when creating a new instance of a shared contract
pub(crate) const INSTANTIATE_SELECTOR: &[u8] = &[156, 163, 60, 158];
pub(crate) fn forbid_execute_operation_origin(vec: &[u8]) -> Result<(), EvmExecutionError> {
ensure!(
vec != EXECUTE_MESSAGE_SELECTOR,
EvmExecutionError::IllegalOperationCall("function execute_message".to_string(),)
);
ensure!(
vec != PROCESS_STREAMS_SELECTOR,
EvmExecutionError::IllegalOperationCall("function process_streams".to_string(),)
);
ensure!(
vec != SUMMARIZE_EVENTS_SELECTOR,
EvmExecutionError::IllegalOperationCall("function summarize_events".to_string(),)
);
ensure!(
vec != INSTANTIATE_SELECTOR,
EvmExecutionError::IllegalOperationCall("function instantiate".to_string(),)
);
Ok(())
}
pub(crate) fn ensure_message_length(
actual_length: usize,
min_length: usize,
) -> Result<(), EvmExecutionError> {
ensure!(
actual_length >= min_length,
EvmExecutionError::OperationIsTooShort
);
Ok(())
}
pub(crate) fn ensure_selector_presence(View on GitHub (pinned to 6c226ddcb3)
Solutions
- Rename the contract function or change its parameter list so its 4-byte selector no longer collides with instantiate(bytes), execute_message(bytes), process_streams(...), or summarize_events(...)
- Route user operations through a non-reserved dispatcher entry point (e.g. perform_operation(bytes)) and submit that as the operation
- If you actually want shared-contract instantiation, use the application-creation/instantiate flow instead of a plain operation
- Print the first 4 bytes of your operation payload and compare them against the reserved selectors declared in linera-execution/src/evm/inputs.rs:150-166
Example fix
// before: operation payload collides with the reserved instantiate(bytes) selector
let op = instantiateCall { value: args }.abi_encode(); // starts with [156,163,60,158]
client.submit_operation(app_id, op).await?;
// after: dispatch through your own entry point
let op = performOperationCall { args: args }.abi_encode(); // distinct selector
client.submit_operation(app_id, op).await?; Defensive patterns
Strategy: validation
Validate before calling
// Before submitting an EVM operation, reject reserved selectors (Rust client)
const RESERVED: [&[u8]; 4] = &[
&[173, 125, 234, 205], // execute_message(bytes)
&[156, 163, 60, 158], // instantiate(bytes)
// fill process_streams / summarize_events selectors from the SDK constants
];
fn is_reserved_selector(op: &[u8]) -> bool {
op.len() >= 4 && RESERVED.iter().any(|s| *s == &op[..4])
}
fn submit(app_id: ApplicationId, op: Vec<u8>) -> Result<(), String> {
if is_reserved_selector(&op) { return Err("operation uses a reserved selector".into()); }
do_submit(app_id, op)
} Type guard
fn is_illegal_operation_call(err: &ExecutionError) -> bool {
matches!(
err,
ExecutionError::EvmError(EvmExecutionError::IllegalOperationCall(_))
)
} Try / catch
match client.submit_operation(app_id, op).await {
Ok(out) => out,
Err(ref e) if is_illegal_operation_call(e) => {
// recover: rewrite the operation to use a non-reserved entry point
retry_with_dispatcher_entrypoint(op)
}
Err(e) => return Err(e.into()),
} Prevention
- Generate operation payloads only from your own contract ABI, never from the base-contract ABI
- Keep a unit test that asserts every operation your client sends has a selector not in the reserved list
- Document which entry points your application exposes for operations versus system calls
When it happens
Trigger: Submitting a user operation (or init transact payload) whose first 4 bytes equal a reserved selector: execute_message(bytes)=[173,125,234,205], process_streams(...), summarize_events(...), or instantiate(bytes)=[156,163,60,158]. Typically happens when the operation payload is built with alloy's instantiateCall abi_encode, or when a ported contract exposes a function whose signature collides with instantiate(bytes).
Common situations: Porting an Ethereum contract that happens to declare instantiate(bytes); forwarding raw user intent into the application without a dispatcher function; hand-crafting operation payloads instead of using ABI-generated wrappers; renaming entry points during an SDK upgrade and reintroducing a collision.
Related errors
- It is illegal to call function execute_message from an opera
- It is illegal to call function process_streams from an opera
- It is illegal to call function summarize_events from an oper
- The balances are incoherent for address {0}, balances {1}, {
- The function {0} is being called but is missing from the byt
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/0b83f6be410297fd.
Report an issue: GitHub.