linera-io/linera-protocol · error · ExecutionError

UnauthenticatedTransferOwner

UnauthenticatedTransferOwner

Error message

ExecutionError::UnauthenticatedTransferOwner

What it means

A Transfer that spends the chain's own main balance (source == AccountOwner::CHAIN) must be authenticated by a signature. This particular throw fires when the operation carries no authenticated owner at all (authenticated_owner is None), meaning the block or transaction was submitted unsigned. The signer is subsequently also required to be an owner of the chain, which is the companion check at the next line.

Source

Thrown at linera-execution/src/system.rs:740

            Ok(Some(
                OutgoingMessage::new(recipient.chain_id, message).with_kind(MessageKind::Tracked),
            ))
        }
    }

    /// Transfers `amount` from `source` to `recipient`, debiting the source account.
    pub async fn transfer(
        &mut self,
        authenticated_owner: Option<AccountOwner>,
        authenticated_application_id: Option<ApplicationId>,
        source: AccountOwner,
        recipient: Account,
        amount: Amount,
    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
        if source == AccountOwner::CHAIN {
            let authenticated_owner =
                authenticated_owner.ok_or(ExecutionError::UnauthenticatedTransferOwner)?;
            ensure!(
                self.ownership.get().await?.is_owner(&authenticated_owner),
                ExecutionError::UnauthenticatedTransferOwner
            );
        } else {
            ensure!(
                authenticated_owner == Some(source)
                    || authenticated_application_id.map(AccountOwner::from) == Some(source),
                ExecutionError::UnauthenticatedTransferOwner
            );
        }
        ensure!(
            amount > Amount::ZERO,
            ExecutionError::IncorrectTransferAmount
        );
        self.debit(&source, amount).await?;
        self.credit_or_send_message(source, recipient, amount).await
    }

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Sign the block containing the Transfer with a key that is a super-owner or owner of the chain, so authenticated_owner is Some(owner) and ownership.is_owner(owner) holds.
  2. Inspect the chain's ownership configuration (the ChainDescription's owners/super-owner) and either register the intended key or switch to a key already listed.
  3. If the goal was to move a user's funds rather than the chain balance, set the transfer's owner to that user's AccountOwner and have them sign the operation.

Example fix

// before: app moves the chain balance; operation arrived with no signer
runtime.transfer(AccountOwner::CHAIN, recipient, amount);

// after: require a signer up front, or spend the signed owner's own account
let Some(signer) = context.authenticated_signer() else {
    return Err(Error::MissingSigner); // fail fast with a clear message
};
runtime.transfer(signer.into(), recipient, amount);
Defensive patterns

Strategy: validation

Validate before calling

// Chain-balance transfers need a signer that is a chain owner
if source == AccountOwner::CHAIN {
    ensure!(
        authenticated_owner.is_some_and(|owner| ownership.is_owner(&owner)),
        "sign the block with a chain owner key before transferring from the chain balance"
    );
}

Type guard

fn is_unauthenticated_transfer(e: &ExecutionError) -> bool {
    matches!(e, ExecutionError::UnauthenticatedTransferOwner)
}

Try / catch

match result {
    Err(ExecutionError::UnauthenticatedTransferOwner) => {
        // The block had no authenticated owner: rebuild it with the
        // chain owner's signature attached and resubmit.
    }
    Err(e) => return Err(e.into()),
    Ok(value) => { /* ... */ }
}

Prevention

When it happens

Trigger: Submitting SystemOperation::Transfer { owner: AccountOwner::CHAIN, amount, recipient } in an unauthenticated block or transaction (no signature attached), or an application calling ContractRuntime::transfer(AccountOwner::CHAIN, recipient, amount) from an operation context whose authenticated_signer is None.

Common situations: A client builds the transfer but never attaches a signing key, or the wallet's default key is not registered as an owner/super-owner in the chain description; attempting a state change through an unauthenticated GraphQL query or read-only path; an SDK application assumes the operation is signed but it was routed through an unauthenticated entry point.

Understand the failure class

Related errors


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