Skip to content

GET operation fails with 'missing contract parameters' error when joining River room #1838

@sanity

Description

@sanity

Bug Report: GET Operation Fails with "missing contract parameters" (v0.1.25)

Summary

When a user attempts to join a River chat room on v0.1.25 (with actor system enabled by default), the GET operation fails with "missing contract parameters" error during the contract storage phase.

Error Details

2025-09-14T17:28:42.936402Z ERROR freenet::contract::executor::runtime: Failed to process contract handler event error=Failed to handle state storage request: StdContractError(Put { key: ContractKey(c24ab5f89301c9b85614a476e0d5c82ffc3dd60fd05a66b67bb6c949e936f623), cause: "missing contract parameters" })

Root Cause Analysis with Code

The issue occurs in the contract parameter retrieval flow when a peer receives a contract from another peer via GET operation.

1. Where the error is thrown - upsert_contract_state()

crates/core/src/contract/executor/runtime.rs:35-64

async fn upsert_contract_state(
    &mut self,
    key: ContractKey,
    update: Either<WrappedState, StateDelta<'static>>,
    related_contracts: RelatedContracts<'static>,
    code: Option<ContractContainer>,  // <-- This is None when error occurs
) -> Result<UpsertResult, ExecutorError> {
    // ... 
    let params = if let Some(code) = &code {
        code.params()
    } else {
        self.state_store
            .get_params(&key)
            .await
            .map_err(ExecutorError::other)?
            .ok_or_else(|| {
                ExecutorError::request(StdContractError::Put {
                    key,
                    cause: "missing contract parameters".into(),  // <-- ERROR HERE
                })
            })?
    };

2. The problematic function - get_contract_locally()

crates/core/src/contract/executor/runtime.rs:912-926

This function returns None if parameters aren't found, even if contract code exists:

async fn get_contract_locally(
    &self,
    key: &ContractKey,
) -> Result<Option<ContractContainer>, ExecutorError> {
    let Some(parameters) = self
        .state_store
        .get_params(key)
        .await
        .map_err(ExecutorError::other)?
    else {
        return Ok(None);  // <-- Returns None if params not in state_store!
    };
    let Some(contract) = self.runtime.contract_store.fetch_contract(key, &parameters) else {
        return Ok(None);
    };
    Ok(Some(contract))
}

3. How GET operation retrieves and stores contracts

crates/core/src/operations/get.rs:526-529

First, GET tries to fetch the contract with code:

// get.rs:526-529
let get_result = op_manager
    .notify_contract_handler(ContractHandlerEvent::GetQuery {
        key,
        return_contract_code: fetch_contract,  // <-- Set to true
    })
    .await;

This eventually calls fetch_contract() runtime.rs:870-910:

async fn fetch_contract(
    &mut self,
    key: ContractKey,
    return_contract_code: bool,
) -> Result<StoreResponse, ExecutorError> {
    // ...
    let contract = if return_contract_code {
        self.get_contract_locally(&key)
            .await?  // <-- Returns None if params missing!
    } else {
        None
    };
    // ...
    Ok(StoreResponse { state, contract })
}

Later, when storing the contract locally get.rs:931-938:

let res = op_manager
    .notify_contract_handler(ContractHandlerEvent::PutQuery {
        key,
        state: value.clone(),
        related_contracts: RelatedContracts::default(),
        contract: contract.clone(),  // <-- This is None due to missing params!
    })
    .await?;

4. The contract store's fetch method

crates/core/src/wasm_runtime/contract_store.rs:203-222

The fetch_contract() in ContractStore requires both key AND parameters:

pub fn fetch_contract(
    &self,
    key: &ContractKey,
    params: &Parameters<'_>,  // <-- Needs params to fetch!
) -> Option<ContractContainer> {
    let result = key
        .code_hash()
        .and_then(|code_hash| {
            self.contract_cache.get(code_hash).map(|data| {
                Some(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
                    WrappedContract::new(data.value().clone(), params.clone().into_owned()),
                )))
            })
        })
        .flatten();
    // ...
}

The Problem Sequence

  1. Peer A has a contract with parameters stored
  2. Peer B requests the contract via GET operation
  3. Peer A responds with contract (state + code)
  4. Peer B tries to store it locally:
    • Calls fetch_contract() with return_contract_code=true
    • get_contract_locally() checks for params in state_store
    • No params found (contract just arrived from network!)
    • Returns None for contract
  5. PutQuery receives contract=None
  6. upsert_contract_state() needs params but can't get them
  7. Fails with "missing contract parameters"

Why This Breaks Now

In v0.1.25 with the actor system, the timing and execution flow may have changed, exposing this latent issue where parameters aren't persisted before they're needed.

Key Insight

The code assumes parameters are already in state_store when get_contract_locally() is called, but for contracts received from remote peers, the parameters haven't been stored yet. The contract exists in the contract_store cache but can't be retrieved without the parameters.

Proposed Fix Options

  1. Store parameters when receiving contract from network - Ensure params are persisted before attempting to use the contract
  2. Modify get_contract_locally() - Allow it to extract params from the contract in contract_store if not in state_store
  3. Change the flow - Store the complete contract (with params) when first received from network

Request for Feedback

@nacho - Since you wrote much of this contract handling code, could you review this analysis? Specifically:

  1. Is the assumption that params should already be in state_store correct?
  2. Should get_contract_locally() handle the case of missing params differently?
  3. What's the intended flow for storing contracts received from other peers?

[AI-assisted debugging and analysis]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions