linera-io/linera-protocol · critical

Failed to deserialize `Operation` in execute_operation

Error message

Failed to deserialize `Operation` in execute_operation

What it means

This is the contract-side boilerplate generated by linera_sdk::contract! (linera-sdk/src/contract/mod.rs). When the runtime dispatches execute_operation, the raw operation bytes are BCS-deserialized into the contract's declared Operation type and success is expected. The panic fires when the bytes do not decode into that type - i.e. the application that created the operation and this contract bytecode disagree on the Operation schema. The panic aborts the contract call and fails the executing block.

Source

Thrown at linera-sdk/src/contract/mod.rs:64

                use $crate::util::BlockingWait as _;
                $crate::contract::run_async_entrypoint::<$contract, _, _>(
                    unsafe { &mut CONTRACT },
                    move |contract| {
                        let argument = $crate::serde_json::from_slice(&argument)
                            .unwrap_or_else(|_| panic!("Failed to deserialize instantiation argument {argument:?}"));

                        contract.instantiate(argument).blocking_wait()
                    },
                )
            }

            fn execute_operation(operation: Vec<u8>) -> Vec<u8> {
                use $crate::util::BlockingWait as _;
                $crate::contract::run_async_entrypoint::<$contract, _, _>(
                    unsafe { &mut CONTRACT },
                    move |contract| {
                        let operation = <$contract as $crate::abi::ContractAbi>::deserialize_operation(operation)
                            .expect("Failed to deserialize `Operation` in execute_operation");

                        let response = contract.execute_operation(operation).blocking_wait();

                        <$contract as $crate::abi::ContractAbi>::serialize_response(response)
                            .expect("Failed to serialize `Response` in execute_operation")
                    },
                )
            }

            fn execute_message(message: Vec<u8>) {
                use $crate::util::BlockingWait as _;
                $crate::contract::run_async_entrypoint::<$contract, _, _>(
                    unsafe { &mut CONTRACT },
                    move |contract| {
                        let message: <$contract as $crate::Contract>::Message =
                            $crate::bcs::from_bytes(&message)
                                .expect("Failed to deserialize message");

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Publish the application again so the operation sender and the handling contract share identical bytecode and Operation types.
  2. Keep Operation (and all ABI types) in the application's shared crate and use that crate on both sides.
  3. Never reuse an application ID after changing Operation; publish as a new application.
  4. Add a unit test round-tripping your operations: bcs::to_bytes then from_bytes::<Operation>, to catch schema drift before deployment.

Example fix

// before: frontend encodes with its own struct
let bytes = bcs::to_bytes(&MyOperationV1 { ... })?;
app.call(bytes).await?; // contract expects MyOperationV2 -> panic

// after: both sides use the shared ABI crate
use my_app::Operation;
let bytes = bcs::to_bytes(&Operation::Transfer { ... })?;
app.call(bytes).await?;
Defensive patterns

Strategy: validation

Validate before calling

// On the sender side, round-trip the operation against the shared ABI type before sending:
use my_app_abi::Operation; // the exact crate the contract compiles against
let bytes = bcs::to_bytes(&operation)?;
bcs::from_bytes::<Operation>(&bytes)?; // if this fails locally, it would panic on-chain

Type guard

fn operation_round_trips(op: &Operation) -> bool {
    bcs::to_bytes(op).ok().and_then(|b| bcs::from_bytes::<Operation>(&b).ok()).is_some()
}

Try / catch

// The panic occurs inside the on-chain contract runtime; callers cannot catch it.
// Guard at the boundary: only send operations built from the application's own ABI crate,
// and verify application bytecode hashes match the published modules before calling.

Prevention

When it happens

Trigger: An operation is posted through an application whose bytecode/ABI differs from the contract executing it: an upgraded application version changed the Operation type while old bytecode still handles calls, mismatched WASM modules published under the same application, or hand-crafted operation bytes sent via request-application/publish paths.

Common situations: Redeploying an application with modified Operation types but reusing the old application ID; publishing mismatched contract/service bytecode pairs; version drift between the dApp frontend (encoding operations) and the on-chain contract.

Related errors


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