{"record":{"id":"d945bd91ca3ef303","repo":"astrid-runtime/astrid","slug":"signed-channel-same-generation-equivocation-reject","errorCode":null,"errorMessage":"signed channel same-generation equivocation rejected","messagePattern":"signed channel same-generation equivocation rejected","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":758,"sourceCode":"    let text =\n        std::str::from_utf8(&previous_bytes).context(\"accepted channel state is not UTF-8\")?;\n    let previous: ChannelPointer =\n        toml::from_str(text).context(\"accepted channel state is invalid TOML\")?;\n    validate_pointer(&previous, channel, None)?;\n    enforce_continuity_values(candidate, candidate_bytes, &previous, &previous_bytes)\n}\n\nfn enforce_continuity_values(\n    candidate: &ChannelPointer,\n    candidate_bytes: &[u8],\n    previous: &ChannelPointer,\n    previous_bytes: &[u8],\n) -> anyhow::Result<()> {\n    if candidate.generation < previous.generation {\n        bail!(\"signed channel generation rollback rejected\");\n    }\n    if candidate.generation == previous.generation && candidate_bytes != previous_bytes {\n        bail!(\"signed channel same-generation equivocation rejected\");\n    }\n    Ok(())\n}\n\npub(super) fn persist_accepted(\n    channel: UpdateChannel,\n    pointer: &[u8],\n    bundle: &[u8],\n) -> anyhow::Result<()> {\n    let (pointer_path, bundle_path) = state_paths(channel)?;\n    let dir = pointer_path\n        .parent()\n        .ok_or_else(|| anyhow::anyhow!(\"channel state path has no parent\"))?;\n    std::fs::create_dir_all(dir).context(\"could not create channel state directory\")?;\n    // The pointer is the continuity commit marker and is replaced last. A\n    // crash after the bundle write leaves the prior accepted generation in\n    // force; the next locked resolution can safely repair the bundle.\n    atomic_write(&bundle_path, bundle)?;","sourceCodeStart":740,"sourceCodeEnd":776,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L740-L776","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet candidate = ChannelPointer { generation: previous.generation, ..changed_content };\n// after\nlet candidate = ChannelPointer { generation: previous.generation + 1, ..changed_content };","handlingStrategy":"validation","validationCode":"fn no_equivocation(candidate: &ChannelPointer, prev: &ChannelPointer, cand_bytes: &[u8], prev_bytes: &[u8]) -> bool { candidate.generation != prev.generation || cand_bytes == prev_bytes }","typeGuard":null,"tryCatchPattern":"if candidate.generation == previous.generation && candidate_bytes != previous_bytes {\n    eprintln!(\"content changed without a generation bump; refusing\");\n    return Ok(ExitCode::FAILURE);\n}","preventionTips":["Bump generation on every content change","Use deterministic serialization (sorted keys, no timestamps)","Acquire a publish lock to prevent concurrent same-generation publishers"],"tags":["security","equivocation","update-channel"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}