linera-io/linera-protocol · error · ExecutionError

CrossApplicationCallInFinalize

CrossApplicationCallInFinalize

Error message

ExecutionError::CrossApplicationCallInFinalize { caller_id: Box::new(self.current_application().id), callee_id: Box::new(callee_id) }

What it means

prepare_for_call blocks cross-application calls while the runtime is finalizing: during an application's finalize stage it may touch its own state but must not call other applications, so try_call_application (or create_application) issued from finalize fails with CrossApplicationCallInFinalize, reporting caller and callee ids (runtime.rs:454).

Source

Thrown at linera-execution/src/runtime.rs:454

                self.applications_to_finalize.push(id);
                Ok(entry
                    .insert(LoadedApplication::new(instance, description))
                    .clone())
            }
        }
    }

    /// Configures the runtime for executing a call to a different contract.
    fn prepare_for_call(
        &mut self,
        this: ContractSyncRuntimeHandle,
        authenticated: bool,
        callee_id: ApplicationId,
    ) -> Result<Arc<Mutex<UserContractInstance>>, ExecutionError> {
        self.check_for_reentrancy(callee_id)?;

        ensure!(
            !self.is_finalizing,
            ExecutionError::CrossApplicationCallInFinalize {
                caller_id: Box::new(self.current_application().id),
                callee_id: Box::new(callee_id),
            }
        );

        // Load the application.
        let application = self.load_contract_instance(this, callee_id)?;

        let caller = self.current_application();
        let caller_id = caller.id;
        let caller_signer = caller.signer;
        // Make the call to user code.
        let authenticated_owner = match caller_signer {
            Some(signer) if authenticated => Some(signer),
            _ => None,
        };

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Move the cross-application call out of finalize into the regular operation or message handler
  2. In finalize, only adjust local state; queue the interaction so the next operation performs it
  3. If a shared code path runs in both contexts, gate it on whether the runtime is finalizing

Example fix

// before
fn finalize(&mut self) {
    runtime.try_call_application(other_app, msg)?;
}

// after
fn finalize(&mut self) {
    self.pending.push(msg); // drained by the next operation
}
Defensive patterns

Strategy: validation

Validate before calling

// Application-level guard: keep cross-app calls out of finalize
enum Stage { Operation, Finalize }

fn maybe_call_app(stage: Stage, callee: ApplicationId, arg: Vec<u8>) {
    match stage {
        Stage::Operation => { runtime.try_call_application(callee, arg); }
        Stage::Finalize => { self.pending.push((callee, arg)); } // deferred to next operation
    }
}

Type guard

fn is_cross_app_call_in_finalize(err: &ExecutionError) -> bool {
    matches!(err, ExecutionError::CrossApplicationCallInFinalize { .. })
}

Try / catch

match runtime.try_call_application(callee, arg) {
    Ok(out) => out,
    Err(ref e) if is_cross_app_call_in_finalize(e) => {
        // defer the interaction; finalize must only touch local state
        self.pending_calls.push((callee, arg));
        Ok(vec![])
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Calling try_call_application or create_application from inside an application's finalize handler, e.g. cleanup or notification logic left in finalize that reaches another application.

Common situations: Moving cleanup or notification logic into finalize while leaving cross-app calls in place; frameworks that run shared handler code in both operation and finalize contexts; upgrades where a code path now executes during finalization.

Related errors


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