linera-io/linera-protocol · error

keypair not found for chain `{chain_id}`

Error message

keypair not found for chain `{chain_id}`

What it means

In forget_keys, mutate found the chain entry but the closure chain.owner.take() returned None — the chain exists in the wallet yet has no owner/keypair attached (already forgotten, or a chain tracked watch-only). This inner Option is converted into the 'keypair not found' anyhow error.

Source

Thrown at linera-wallet-json/src/wallet.rs:200

    /// Applies a mutation to the chain with the given ID, saving afterwards. Returns `None`
    /// if the chain is not in the wallet.
    pub fn mutate<R>(
        &self,
        chain_id: ChainId,
        mutate: impl Fn(&mut Chain) -> R,
    ) -> Option<Result<R, persistent::file::Error>> {
        self.0
            .chains
            .mutate(chain_id, mutate)
            .map(|outcome| self.0.save().map(|()| outcome))
    }

    /// Removes and returns the owner of the given chain, erroring if the chain or owner is absent.
    pub fn forget_keys(&self, chain_id: ChainId) -> anyhow::Result<AccountOwner> {
        self.mutate(chain_id, |chain| chain.owner.take())
            .ok_or_else(|| anyhow::anyhow!("nonexistent chain `{chain_id}`"))??
            .ok_or_else(|| anyhow::anyhow!("keypair not found for chain `{chain_id}`"))
    }

    /// Writes the wallet to its file.
    pub fn save(&self) -> Result<(), persistent::file::Error> {
        self.0.save()
    }

    /// Returns the number of chains in the wallet.
    pub fn num_chains(&self) -> usize {
        self.0.chains.items().len()
    }

    /// Returns the IDs of all chains in the wallet.
    pub fn chain_ids(&self) -> Vec<ChainId> {
        self.0.chains.chain_ids()
    }

    /// Returns the list of all chain IDs for which we have a secret key.

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Treat it as already-done if your workflow is idempotent: match the error and continue when the keys were previously forgotten
  2. Inspect wallet chain state first: check whether owned_chain_ids() contains the chain before calling forget_keys
  3. Do not retry blindly — the wallet file was already saved without the owner on the first successful call

Example fix

// before
let owner = wallet.forget_keys(chain_id)?; // second call -> error

// after (idempotent helper)
let owner = match wallet.forget_keys(chain_id) {
    Ok(owner) => owner,
    Err(e) if e.to_string().contains("keypair not found") => {
        return Ok(None); // keys were already forgotten
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Only attempt to forget keys we actually own
if wallet.owned_chain_ids().contains(&chain_id) {
    wallet.forget_keys(chain_id)?;
} else {
    tracing::warn!(%chain_id, "no keys held for chain; skipping");
}

Try / catch

let owner = match wallet.forget_keys(chain_id) {
    Ok(owner) => Some(owner),
    Err(e) if e.to_string().contains("keypair not found") => None, // already forgotten
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: Calling forget_keys twice on the same chain; forgetting keys of a chain the wallet tracks without owning (e.g. a read-only/multi-owner chain where this wallet never held the keypair).

Common situations: Idempotency-unaware scripts that retry a forget operation after a timeout; wallet state changed by another process between listing and forgetting; user assuming 'forget' resets the chain rather than removing key material.

Related errors


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