astrid-runtime/astrid · critical

signed channel same-generation equivocation rejected

Error message

signed channel same-generation equivocation rejected

What it means

When a candidate channel pointer has the same generation number as the previously accepted one, its serialized bytes must be identical; any difference is 'equivocation' — two distinct signed payloads claiming the same generation — and is rejected to keep the channel unambiguous and tamper-evident.

Source

Thrown at crates/astrid-cli/src/commands/update_channel.rs:758

    let text =
        std::str::from_utf8(&previous_bytes).context("accepted channel state is not UTF-8")?;
    let previous: ChannelPointer =
        toml::from_str(text).context("accepted channel state is invalid TOML")?;
    validate_pointer(&previous, channel, None)?;
    enforce_continuity_values(candidate, candidate_bytes, &previous, &previous_bytes)
}

fn enforce_continuity_values(
    candidate: &ChannelPointer,
    candidate_bytes: &[u8],
    previous: &ChannelPointer,
    previous_bytes: &[u8],
) -> anyhow::Result<()> {
    if candidate.generation < previous.generation {
        bail!("signed channel generation rollback rejected");
    }
    if candidate.generation == previous.generation && candidate_bytes != previous_bytes {
        bail!("signed channel same-generation equivocation rejected");
    }
    Ok(())
}

pub(super) fn persist_accepted(
    channel: UpdateChannel,
    pointer: &[u8],
    bundle: &[u8],
) -> anyhow::Result<()> {
    let (pointer_path, bundle_path) = state_paths(channel)?;
    let dir = pointer_path
        .parent()
        .ok_or_else(|| anyhow::anyhow!("channel state path has no parent"))?;
    std::fs::create_dir_all(dir).context("could not create channel state directory")?;
    // The pointer is the continuity commit marker and is replaced last. A
    // crash after the bundle write leaves the prior accepted generation in
    // force; the next locked resolution can safely repair the bundle.
    atomic_write(&bundle_path, bundle)?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Increment the generation number whenever channel content changes
  2. Make channel serialization deterministic (sorted keys, no timestamps) so identical content yields identical bytes
  3. Re-fetch the current channel and base your candidate on the latest generation before editing
  4. Investigate possible concurrent publishers if you did not change the content

Example fix

// before
let candidate = ChannelPointer { generation: previous.generation, ..changed_content };
// after
let candidate = ChannelPointer { generation: previous.generation + 1, ..changed_content };
Defensive patterns

Strategy: validation

Validate before calling

fn no_equivocation(candidate: &ChannelPointer, prev: &ChannelPointer, cand_bytes: &[u8], prev_bytes: &[u8]) -> bool { candidate.generation != prev.generation || cand_bytes == prev_bytes }

Try / catch

if candidate.generation == previous.generation && candidate_bytes != previous_bytes {
    eprintln!("content changed without a generation bump; refusing");
    return Ok(ExitCode::FAILURE);
}

Prevention

When it happens

Trigger: Publishing a candidate with candidate.generation == previous.generation but candidate_bytes != previous_bytes (content changed without bumping generation) — checked in the same helper at update_channel.rs:758.

Common situations: Editing channel contents (asset URLs, versions) but forgetting to increment generation; two publishers building the channel from different sources at the same generation; non-deterministic serialization (timestamps, map ordering) producing different bytes for equivalent content.

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 astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/d945bd91ca3ef303. Report an issue: GitHub.