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

  1. 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(...)
  2. Route user operations through a non-reserved dispatcher entry point (e.g. perform_operation(bytes)) and submit that as the operation
  3. If you actually want shared-contract instantiation, use the application-creation/instantiate flow instead of a plain operation
  4. 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

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


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/0b83f6be410297fd. Report an issue: GitHub.