datahaven-xyz/datahaven · error · Error

SyncCommitteeUpdateRequired

SyncCommitteeUpdateRequired

Error message

SyncCommitteeUpdateRequired

What it means

Thrown in `verify_update` when the update contains no `next_sync_committee_update` and the update's finalized period is not equal to the store period. The light-client protocol requires that an update within the same period include a next-sync-committee payload unless it is finalized to the current store period; otherwise the client cannot advance safely.

Solutions

  1. Submit an update that includes the `next_sync_committee_update` for the rotation period before submitting period-skipping finality updates.
  2. Re-run the bootstrap flow if the client is far behind the sync-committee rotation schedule.
  3. Adjust the relayer to never skip period boundaries when forwarding updates.
  4. Verify `store_period` (period of the latest finalized beacon state) and pick updates finalized within it.

Example fix

// before: finality-only update that jumps a period
submit(finality_update);
// after: include the sync committee rotation
let mut update = beacon.get_update(finalized_slot);
update.next_sync_committee_update = Some(beacon.get_next_sync_committee());
submit(update);
Defensive patterns

Strategy: validation

Validate before calling

const storePeriod = computePeriod(await getStoreSlot());
const finalizedPeriod = computePeriod(update.finalized_header.slot);
if (!update.next_sync_committee_update && finalizedPeriod !== storePeriod) {
  throw new Error('update must include sync committee rotation or stay in store period');
}

Type guard

function isFinalityOnlyInPeriod(update, storePeriod) {
  return update.next_sync_committee_update == null
    && computePeriod(update.finalized_header.slot) === storePeriod;
}

Try / catch

try {
  await submit(update);
} catch (e) {
  if (String(e).includes('SyncCommitteeUpdateRequired')) {
    await submitRotationUpdateThenRetry(update);
  }
}

Prevention

When it happens

Trigger: Submitting a finality-only update (no sync committee data) whose `finalized_header.slot` falls into a period ahead of `store_period`, i.e. the client skipped past a sync-committee rotation without receiving the new committee.

Common situations: Relayer downtime spanning a sync-committee period boundary, replaying an old update after the stored committee rotated, or a light client that missed the initial committee handover update.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of datahaven-xyz/datahaven@edcb13dbbc (2026-09-13). Data as JSON: /api/errors/3ed88e88ab05bf36. Report an issue: GitHub.

Appendix: source

Thrown at operator/pallets/ethereum-client/src/lib.rs:440

                        Error::<T>::InvalidSyncCommitteeUpdate
                    );
                }
                let next_sync_committee_gindex = Self::next_sync_committee_gindex_at_slot(
                    update.attested_header.slot,
                    fork_versions,
                );
                ensure!(
                    verify_merkle_branch(
                        sync_committee_root,
                        &next_sync_committee_update.next_sync_committee_branch,
                        subtree_index(next_sync_committee_gindex),
                        generalized_index_length(next_sync_committee_gindex),
                        update.attested_header.state_root
                    ),
                    Error::<T>::InvalidSyncCommitteeMerkleProof
                );
            } else {
                ensure!(
                    update_finalized_period == store_period,
                    Error::<T>::SyncCommitteeUpdateRequired
                );
            }

            // Verify sync committee aggregate signature.
            let sync_committee = if signature_period == store_period {
                <CurrentSyncCommittee<T>>::get()
            } else {
                <NextSyncCommittee<T>>::get()
            };
            let absent_pubkeys =
                Self::find_pubkeys(&participation, (*sync_committee.pubkeys).as_ref(), false);
            let signing_root = Self::signing_root(
                &update.attested_header,
                Self::validators_root(),
                update.signature_slot,
            )?;

View on GitHub (pinned to edcb13dbbc)