linera-io/linera-protocol · error

Attempt to modify storage from a service

Error message

Attempt to modify storage from a service

What it means

linera-sdk's system API for views dispatches write_batch differently per runtime interface. Contract runtimes forward batches to the host to mutate chain state; service runtimes are read-only by protocol design, so write_batch on WitInterface::Service panics with 'Attempt to modify storage from a service'. Any state mutation must go through contract operations inside blocks.

Source

Thrown at linera-sdk/src/views/system_api.rs:343

            WitInterface::Service => service_wit::find_key_values_wait(promise),
            #[cfg(with_testing)]
            WitInterface::Mock { store, .. } => store.find_key_values_wait(promise),
        }
    }

    /// Calls the `write_batch` WIT function.
    fn write_batch(&self, batch: Batch) {
        match self {
            WitInterface::Contract => {
                let batch_operations = batch
                    .operations
                    .into_iter()
                    .map(WriteOperation::from)
                    .collect::<Vec<_>>();

                contract_runtime_api::write_batch(&batch_operations);
            }
            WitInterface::Service => panic!("Attempt to modify storage from a service"),
            #[cfg(with_testing)]
            WitInterface::Mock {
                store,
                read_only: false,
            } => {
                store.write_batch(batch);
            }
            #[cfg(with_testing)]
            WitInterface::Mock {
                read_only: true, ..
            } => {
                panic!("Attempt to modify storage from a service")
            }
        }
    }
}

/// Implementation of [`linera_views::context::Context`] to be used for data storage

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Move the mutating logic into a contract operation; services may only read views
  2. In the service, replace save()/write calls with read-only getters (get, load, etc.)
  3. If a query should change state, have the client submit the corresponding operation via GraphQL instead
  4. Keep shared state-manipulation code in modules callable only from the contract side

Example fix

// before (service handle_query)
async fn handle_query(&self, _: Query) -> Result<...> {
    self.counter.set(self.counter.get().await? + 1);
    self.counter.save().await?; // panics: write_batch on service
}

// after (read-only service)
async fn handle_query(&self, _: Query) -> Result<...> {
    Ok(self.counter.get().await?)
}
// increments happen in the contract's operation entrypoint
Defensive patterns

Strategy: validation

Validate before calling

// In the service, only touch read APIs; guard against accidental persistence:
#[cfg(debug_assertions)]
fn assert_read_only<T: std::fmt::Debug>(_: &T) {} // placeholder for lint hooks
// Review rule: handle_query must never call `.save().await` or mutate collections.

Prevention

When it happens

Trigger: Inside a service's handle_query (or any service-side code), calling a view method that persists: view.save().await, ViewStorage write operations, queue pushes — anything that eventually calls write_batch on the service interface.

Common situations: Porting a contract method into the service; forgetting that services answer queries without creating blocks; attempting to cache/memoize state in views during queries.

Related errors


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