linera-io/linera-protocol · error · ExecutionError

InvalidCommitteeRemoval

InvalidCommitteeRemoval

Error message

ExecutionError::InvalidCommitteeRemoval

What it means

RemoveCommittee deregisters an old committee (epoch) on the admin chain. The system tracks how many epochs were already removed in the REMOVED_EPOCH_STREAM_NAME system stream; the check at system.rs:539 requires count == epoch.0 AND epoch < current epoch. So removals must start at epoch 0 and proceed strictly one-by-one (0, 1, 2, ...) with no repeats or gaps, and a newer committee must already be active before the old one is removed.

Source

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

                            .await?;
                        self.blob_used(txn_tracker, blob_id).await?;
                        self.committee_hash.set(Some(blob_hash));
                        self.epoch.set(epoch);
                        let event_data = EpochEventData {
                            blob_hash,
                            timestamp: context.timestamp,
                        };
                        let stream_id = StreamId::system(EPOCH_STREAM_NAME);
                        let next_index = epoch.0.checked_add(1).ok_or(ArithmeticError::Overflow)?;
                        self.stream_event_counts.insert(&stream_id, next_index)?;
                        txn_tracker.add_event(stream_id, epoch.0, bcs::to_bytes(&event_data)?);
                    }
                    AdminOperation::RemoveCommittee { epoch } => {
                        let stream_id = StreamId::system(REMOVED_EPOCH_STREAM_NAME);
                        let count = self.stream_event_counts.get(&stream_id).await?.unwrap_or(0);
                        // Revocations must happen in increasing epoch order, so the stream's
                        // indices stay sequential.
                        ensure!(
                            count == epoch.0 && epoch < *self.epoch.get(),
                            ExecutionError::InvalidCommitteeRemoval
                        );
                        let next_index = epoch.0.checked_add(1).ok_or(ArithmeticError::Overflow)?;
                        self.stream_event_counts.insert(&stream_id, next_index)?;
                        txn_tracker.add_event(stream_id, epoch.0, vec![]);
                    }
                }
            }
            PublishModule { module_id } => {
                for blob_id in module_id.bytecode_blob_ids() {
                    self.blob_published(&blob_id, txn_tracker)?;
                }
            }
            CreateApplication {
                module_id,
                parameters,
                instantiation_argument,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Create the next committee first (CreateCommittee with the next epoch) so the epoch to remove is strictly older than the current one.
  2. Remove epochs one at a time in increasing order, starting from the current removal count (first removal must be epoch 0).
  3. Query the admin chain's REMOVED_EPOCH_STREAM_NAME event count (or the chain state) to learn the next expected epoch before submitting.
  4. Drop duplicated or replayed removal operations from pending batches; each removal is single-use.

Example fix

// before: removing the current committee (no newer committee yet)
let op = AdminOperation::RemoveCommittee { epoch: current_epoch }; // InvalidCommitteeRemoval

// after: rotate first, then remove the old epoch in order
// 1) AdminOperation::CreateCommittee { epoch: current_epoch + 1, blob_hash }
// 2) AdminOperation::RemoveCommittee { epoch: current_epoch }
Defensive patterns

Strategy: validation

Validate before calling

// derive the expected next removable epoch from the removal stream before submitting
let count = node
    .events(admin_chain_id, StreamId::system(REMOVED_EPOCH_STREAM_NAME))
    .await?.len() as u32; // prior removals
let epoch_to_remove = count; // must equal count and be < current epoch
assert!(Epoch(epoch_to_remove) < current_epoch);

Try / catch

match result {
    Err(ExecutionError::InvalidCommitteeRemoval) => {
        // re-read removal count and current epoch, then resubmit the correct epoch
    }
    other => other,
}

Prevention

When it happens

Trigger: AdminOperation::RemoveCommittee { epoch } where epoch does not equal the number of prior removals (skipping an epoch, removing out of order, or re-submitting an already-removed epoch), or where epoch is greater than or equal to the chain's current epoch (attempting to remove the currently active committee before a newer one was created).

Common situations: Committee-rotation scripts that remove epochs in bulk or assume a different ordering; retrying a removal that already executed in an earlier block; removing the current committee immediately after publishing but before creating the next one; test harnesses jumping epochs non-sequentially.

Related errors


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