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.

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.

Appendix: 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)