{"record":{"id":"4aca0f052ae02bf1","repo":"linera-io/linera-protocol","slug":"epoch-is-already-revoked","errorCode":null,"errorMessage":"Epoch is already revoked","messagePattern":"Epoch is already revoked","errorType":"validation","errorClass":"ChainClientError","httpStatus":null,"severity":"warning","filePath":"linera-core/src/client/chain_client/mod.rs","lineNumber":2976,"sourceCode":"        let current_epoch = self.chain_info().await?.epoch;\n        ensure!(\n            revoked_epoch < current_epoch,\n            Error::CannotRevokeCurrentEpoch(current_epoch)\n        );\n        let mut operations = Vec::new();\n        for epoch_index in 0..=revoked_epoch.0 {\n            let epoch = Epoch(epoch_index);\n            if self\n                .has_admin_event(REMOVED_EPOCH_STREAM_NAME, epoch.0)\n                .await?\n            {\n                continue;\n            }\n            operations.push(Operation::system(SystemOperation::Admin(\n                AdminOperation::RemoveCommittee { epoch },\n            )));\n        }\n        ensure!(!operations.is_empty(), Error::EpochAlreadyRevoked);\n        self.execute_operations(operations, vec![]).await\n    }\n\n    /// Sends money to a chain.\n    /// Do not check balance. (This may block the client)\n    /// Do not confirm the transaction.\n    #[cfg(with_testing)]\n    #[instrument(level = \"trace\")]\n    pub async fn transfer_to_account_unsafe_unconfirmed(\n        &self,\n        owner: AccountOwner,\n        amount: Amount,\n        recipient: Account,\n    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {\n        self.execute_operation(SystemOperation::Transfer {\n            owner,\n            recipient,\n            amount,","sourceCodeStart":2958,"sourceCodeEnd":2994,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-core/src/client/chain_client/mod.rs#L2958-L2994","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Treat the error as success: the revocation you asked for is already in place, so catch Error::EpochAlreadyRevoked and return the previous outcome","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","If your intent is to revoke more, pass a higher revoked_epoch that is still below the current epoch"],"exampleFix":"// before\nlet outcome = client.revoke_epochs(epoch).await?; // Err(EpochAlreadyRevoked) on retry\n\n// after\nlet outcome = match client.revoke_epochs(epoch).await {\n    Err(chain_client::Error::EpochAlreadyRevoked) => {\n        // All epochs <= `epoch` are already revoked; nothing to do.\n        return Ok(ClientOutcome::Committed(previous_certificate.clone()));\n    }\n    result => result?,\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn is_epoch_already_revoked(err: &chain_client::Error) -> bool {\n    matches!(err, chain_client::Error::EpochAlreadyRevoked)\n}","tryCatchPattern":"match client.revoke_epochs(epoch).await {\n    Err(chain_client::Error::EpochAlreadyRevoked) => {\n        // Goal already achieved on chain; treat as idempotent success.\n    }\n    other => other?,\n}","preventionTips":["In admin automation, remember the highest revoked epoch per chain and skip re-invoking revoke_epochs with the same or lower value","Treat revoke_epochs as an idempotent operation: always match on EpochAlreadyRevoked and continue","Before scripting revocation, read the current epoch and the admin chain's REMOVED_EPOCH_STREAM_NAME events to compute which epochs still need removing"],"tags":["linera","admin-chain","epoch","committee","idempotency"],"backgroundTag":"idempotency-conflict","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}