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
- Increment the generation number whenever channel content changes
- Make channel serialization deterministic (sorted keys, no timestamps) so identical content yields identical bytes
- Re-fetch the current channel and base your candidate on the latest generation before editing
- 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
- Bump generation on every content change
- Use deterministic serialization (sorted keys, no timestamps)
- Acquire a publish lock to prevent concurrent same-generation publishers
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
- signed channel generation rollback rejected
- {label} release metadata identity is invalid
- {label} release metadata does not match the authenticated le
- {label} release metadata does not bind the authenticated leg
- manifest exceeds its installed capability approval: {details
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/d945bd91ca3ef303.
Report an issue: GitHub.