linera-io/linera-protocol · warning · ChainClientError

Epoch is already revoked

Error message

Epoch is already revoked

What it means

Thrown by ChainClient::revoke_epochs (admin chains only) when every epoch from 0 up to and including the requested revoked_epoch already has a RemoveCommittee admin event on the REMOVED_EPOCH_STREAM_NAME stream. The method builds one RemoveCommittee operation per still-active epoch; if the loop skipped all of them, `operations` is empty and the guard fires instead of submitting an empty block. It is an idempotency guard: the requested revocation goal is already fully achieved on chain.

Source

Thrown at linera-core/src/client/chain_client/mod.rs:2976

        let current_epoch = self.chain_info().await?.epoch;
        ensure!(
            revoked_epoch < current_epoch,
            Error::CannotRevokeCurrentEpoch(current_epoch)
        );
        let mut operations = Vec::new();
        for epoch_index in 0..=revoked_epoch.0 {
            let epoch = Epoch(epoch_index);
            if self
                .has_admin_event(REMOVED_EPOCH_STREAM_NAME, epoch.0)
                .await?
            {
                continue;
            }
            operations.push(Operation::system(SystemOperation::Admin(
                AdminOperation::RemoveCommittee { epoch },
            )));
        }
        ensure!(!operations.is_empty(), Error::EpochAlreadyRevoked);
        self.execute_operations(operations, vec![]).await
    }

    /// Sends money to a chain.
    /// Do not check balance. (This may block the client)
    /// Do not confirm the transaction.
    #[cfg(with_testing)]
    #[instrument(level = "trace")]
    pub async fn transfer_to_account_unsafe_unconfirmed(
        &self,
        owner: AccountOwner,
        amount: Amount,
        recipient: Account,
    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
        self.execute_operation(SystemOperation::Transfer {
            owner,
            recipient,
            amount,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Treat the error as success: the revocation you asked for is already in place, so catch Error::EpochAlreadyRevoked and return the previous outcome
  2. Before calling, query the chain's admin stream (REMOVED_EPOCH_STREAM_NAME events) or the current committee state to see which epochs are still active
  3. If your intent is to revoke more, pass a higher revoked_epoch that is still below the current epoch

Example fix

// before
let outcome = client.revoke_epochs(epoch).await?; // Err(EpochAlreadyRevoked) on retry

// after
let outcome = match client.revoke_epochs(epoch).await {
    Err(chain_client::Error::EpochAlreadyRevoked) => {
        // All epochs <= `epoch` are already revoked; nothing to do.
        return Ok(ClientOutcome::Committed(previous_certificate.clone()));
    }
    result => result?,
};
Defensive patterns

Strategy: try-catch

Type guard

fn is_epoch_already_revoked(err: &chain_client::Error) -> bool {
    matches!(err, chain_client::Error::EpochAlreadyRevoked)
}

Try / catch

match client.revoke_epochs(epoch).await {
    Err(chain_client::Error::EpochAlreadyRevoked) => {
        // Goal already achieved on chain; treat as idempotent success.
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling client.revoke_epochs(epoch) where every epoch 0..=epoch was already revoked earlier (e.g. a script or CLI re-runs the same admin command). Also revoked_epoch must be < current epoch, otherwise CannotRevokeCurrentEpoch fires first, so this error specifically means 'old epochs, all already removed'.

Common situations: Automation/wallet tooling that retries epoch revocation after a first success; operators passing a stale epoch value read from an old config; idempotent deployment scripts that assume re-invocation is a no-op but don't handle the error variant.

Related errors


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