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
- Publish the application again so the operation sender and the handling contract share identical bytecode and Operation types.
- Keep Operation (and all ABI types) in the application's shared crate and use that crate on both sides.
- Never reuse an application ID after changing Operation; publish as a new application.
- 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
- Generate operation bytes exclusively via the application's shared ABI crate; never hand-encode.
- Publish contract and service bytecode as a matched pair from one build.
- Treat any Operation type change as a new application: publish fresh instead of reusing IDs.
- Add a CI test that BCS round-trips every Operation variant.
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
- Failed to deserialize message
- Application parameters must be deserializable
- Failed to deserialize `Response` in cross-application call
- owner should be different from spender
- invalid block export configuration: {message}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/b249160934f48a43.
Report an issue: GitHub.