From d8044fcf5d3ecdb54430601e36d3e48ecc909e3d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 9 Sep 2026 11:13:03 +0200 Subject: [PATCH] Ask the cursor nothing: admit against the declared incarnation c09993c replaced the gate without asking what the gate was for, and kept comparing a receipt to a cursor -- just loosely enough to survive. That is the compensator shape, so this deletes it instead. latest_odin_observation is not a receipt. It is a replay cursor: it is written in lockstep with odin_publisher_sequence_cursor in both places it is set (:3212, :4815), and AdmittedGeneration::validate requires the two to agree. The field's own validator says what it is for. The commit gate used it as proof of readiness, which conflates a high-water mark with a proof -- and because TopologyEvidence derives PartialEq over five fields, the comparison also demanded the two records agree on admitted_at_unix_millis, which is Idunn's own wall clock. Admission does not need a second observation to compare against. It needs one question answered: does Odin, right now, say the incarnation this transaction declared it was deploying is ready? The declaration is the transaction's own Expected projection and prepared activation. observation_describes_declared_incarnation checks target, Expected projection digest and runtime instance against those; is_semantic_ready supplies the "is ready" half. latest_still_confirms_ready, latest_is_not_older and same_incarnation are deleted -- anti-replay is already sequence_requires_admission plus the cursor, which is the cursor's actual job. The same conflation was copy-pasted into rehydrate_admitted_ready, so any managed provider dropped out of the dependency-provider set the moment it published anything. Unnoticed only because nothing in production has a managed dependency yet. Verified against the live store: old gate (receipt == cursor): REFUSE cursor says: projection=sha256-1f855526... instance=sha256-25f32a8d... ready=true declared: projection=sha256-1f855526... instance=sha256-25f32a8d... new gate: PASS cargo test --locked -> test result: ok. 100 passed; 0 failed; 1 ignored Co-Authored-By: Claude Opus 5 --- src/control_plane.rs | 270 ++++++++++++++++++++++--------------------- 1 file changed, 140 insertions(+), 130 deletions(-) diff --git a/src/control_plane.rs b/src/control_plane.rs index 64dc456..e82bfd0 100644 --- a/src/control_plane.rs +++ b/src/control_plane.rs @@ -4008,13 +4008,10 @@ impl Engine { is_semantic_ready(&authenticated), "latest Odin observation is not Ready at admission commit" ); - latest_still_confirms_ready( - required(&ready_current.value.ready, "Ready receipt")?, - required( - &ready_current.value.latest_odin_observation, - "latest Odin receipt", - )?, - &authenticated, + observation_describes_declared_incarnation( + authenticated.record(), + required(&ready_current.value.expected, "Expected projection")?, + required(&ready_current.value.activation, "activation")?, ) .context("at admission commit")?; self.rehydrate_ready_token(&ready_current.value, now_millis()?, true)?; @@ -4084,13 +4081,10 @@ impl Engine { is_semantic_ready(&authenticated), "latest Odin observation is not Ready after the final admission challenge" ); - latest_still_confirms_ready( - required(&commit_current.value.ready, "Ready receipt")?, - required( - &commit_current.value.latest_odin_observation, - "latest Odin receipt", - )?, - &authenticated, + observation_describes_declared_incarnation( + authenticated.record(), + required(&commit_current.value.expected, "Expected projection")?, + required(&commit_current.value.activation, "activation")?, ) .context("after the final admission challenge")?; let now = now_millis()?; @@ -4972,10 +4966,11 @@ impl Engine { is_semantic_ready(&authenticated), "admitted provider no longer has current exact Ready evidence" ); - // A provider that has published since it went ready is the ordinary - // case, not a stale one. Authenticate what it last said and require - // that it still describes this incarnation and still says ready -- - // the Ready receipt above remains the evidence a dependant cites. + // A provider that published after going ready is the ordinary case, not + // a stale one, so the cursor is checked on its own terms rather than + // against the receipt. Note this is the cursor as frozen at admission, + // not a live read of Odin -- dependency evidence is stored, and making + // it current is a separate question this does not answer. let authenticated_latest = authenticate_odin_runtime_topology_correlation( &generation.latest_odin_observation.canonical_bytes, &authority, @@ -4988,10 +4983,10 @@ impl Engine { is_semantic_ready(&authenticated_latest), "admitted provider's latest Odin observation is no longer Ready" ); - latest_still_confirms_ready( - &generation.ready, - &generation.latest_odin_observation, - &authenticated_latest, + observation_describes_declared_incarnation( + authenticated_latest.record(), + &generation.expected, + &generation.activation, )?; Ok(SequenceAdmittedReady { transaction_id: generation.transaction_id.clone(), @@ -5713,64 +5708,39 @@ fn warming_disagreements_match_incumbent( } } -/// Odin republishes a correlation whenever any fact it observes changes: a -/// dependency becoming ready, a presence sequence advancing, a capability set -/// widening. Requiring the latest observation to equal the Ready receipt -/// therefore requires the observer to have stopped observing, which is -/// unsatisfiable for any live target. An admission that reached Committing -/// could never leave it: on yggdrasil, odin sat there with `ready` at sequence -/// 3841 and `latest` at 210313, while the candidate it deployed was serving. +/// `latest_odin_observation` is a replay cursor, not a receipt. It is written +/// in lockstep with `odin_publisher_sequence_cursor` in both places it is set, +/// and `AdmittedGeneration::validate` requires the two to agree -- the field's +/// own validator says what it is for. Comparing it to the Ready receipt +/// conflated a high-water mark with a proof, and because `TopologyEvidence` +/// derives `PartialEq` over five fields the comparison also demanded the two +/// records agree on `admitted_at_unix_millis`: Idunn's own wall clock. On +/// yggdrasil that gate held tx-65574579 in Committing with ready at sequence +/// 3841 against a cursor at 297765 and rising, while the candidate it deployed +/// served traffic. /// -/// The gate was standing in for something true and narrower -- the newest thing -/// Odin says about this target still describes the incarnation we proved ready. -/// That is identity of the subject, not equality of the statement. Callers pair -/// this with `is_semantic_ready` on the same authenticated record, which is what -/// establishes that the newer statement still says ready. -fn latest_still_confirms_ready( - ready: &TopologyEvidence, - latest: &TopologyEvidence, - authenticated_latest: &AuthenticatedOdinRuntimeTopologyCorrelation, -) -> Result<()> { - latest_is_not_older(ready, latest)?; - // The Ready receipt was authenticated when it was admitted and is read back - // here only to name the incarnation it described. A forged receipt cannot - // reach this function: it would have failed admission, and the record it is - // compared against is the authenticated one. - let (ready_record, _) = OdinRuntimeTopologyCorrelationRecord::decode_canonical_signed_payload( - &ready.canonical_bytes, - )?; - same_incarnation(&ready_record, authenticated_latest.record()) -} - -/// Split out so the ordering guard is testable without a signed correlation: -/// Idunn's tests cannot construct one, which is a large part of why the gate -/// this replaces was never covered. -fn latest_is_not_older(ready: &TopologyEvidence, latest: &TopologyEvidence) -> Result<()> { - ensure!( - latest.publisher_sequence >= ready.publisher_sequence, - "latest Odin observation is older than the Ready receipt" - ); - ensure!( - latest.signer_identity_id == ready.signer_identity_id, - "latest Odin observation carries a different publisher than the Ready receipt" - ); - Ok(()) -} - -/// Identity of the subject: same target, same Expected projection, same running -/// instance. A newer statement about *this* incarnation is Odin working. A -/// statement about a different one means the thing we proved ready is not the -/// thing now running, which is the failure the old equality gate was reaching -/// for and the only one it should have caught. -fn same_incarnation( - ready: &OdinRuntimeTopologyCorrelationRecord, - latest: &OdinRuntimeTopologyCorrelationRecord, +/// Admission does not need a second observation to compare against. It needs +/// one question answered: does Odin, right now, say that the incarnation this +/// transaction declared it was deploying is ready? The declaration is the +/// transaction's own Expected projection and prepared activation -- not another +/// observation of them. Callers pair this with `is_semantic_ready` on the same +/// authenticated record, which supplies the "is ready" half. +fn observation_describes_declared_incarnation( + observed: &OdinRuntimeTopologyCorrelationRecord, + expected: &IdunnExpectedIncarnationRecord, + activation: &IdunnRuntimeActivationRecord, ) -> Result<()> { ensure!( - latest.target == ready.target - && latest.expected_projection_sha256 == ready.expected_projection_sha256 - && latest.runtime_instance_id == ready.runtime_instance_id, - "latest Odin observation describes a different incarnation than the Ready receipt" + observed.target == expected.target, + "Odin observation describes another target" + ); + ensure!( + observed.expected_projection_sha256 == expected.canonical_sha256()?, + "Odin observation describes another Expected projection" + ); + ensure!( + observed.runtime_instance_id.as_deref() == Some(activation.runtime_instance_id.as_str()), + "Odin observation describes another runtime instance" ); Ok(()) } @@ -6022,35 +5992,86 @@ mod tests { value.transaction_id, value.target, value.phase, ready.publisher_sequence, latest.publisher_sequence ); - println!(" old gate (byte equality): {}", if ready == latest { "PASS" } else { "REFUSE" }); - let order = latest_is_not_older(ready, latest); - println!(" new gate, ordering: {:?}", order.as_ref().map(|_| "PASS")); - let (ready_record, _) = - OdinRuntimeTopologyCorrelationRecord::decode_canonical_signed_payload( - &ready.canonical_bytes, - )?; + println!(" old gate (receipt == cursor): {}", if ready == latest { "PASS" } else { "REFUSE" }); let (latest_record, _) = OdinRuntimeTopologyCorrelationRecord::decode_canonical_signed_payload( &latest.canonical_bytes, )?; println!( - " ready: projection={} instance={:?} ready_flag={}", - ready_record.expected_projection_sha256, ready_record.runtime_instance_id, - ready_record.ready - ); - println!( - " latest: projection={} instance={:?} ready_flag={} present={} disagreements={}", + " cursor says: projection={} instance={:?} ready={} present={} disagreements={}", latest_record.expected_projection_sha256, latest_record.runtime_instance_id, latest_record.ready, latest_record.present, latest_record.disagreements.len() ); + let (Some(expected), Some(activation)) = (&value.expected, &value.activation) else { + println!(" transaction has not declared an incarnation yet"); + continue; + }; println!( - " new gate, incarnation: {:?}", - same_incarnation(&ready_record, &latest_record).map(|_| "PASS") + " declared: projection={} instance={}", + expected.canonical_sha256()?, activation.runtime_instance_id + ); + println!( + " new gate: {:?}", + observation_describes_declared_incarnation(&latest_record, expected, activation) + .map(|_| "PASS") ); } Ok(()) } + fn declared_incarnation() -> (IdunnExpectedIncarnationRecord, IdunnRuntimeActivationRecord) { + let expected = IdunnExpectedIncarnationRecord { + schema_version: cultnet_rs::IDUNN_EXPECTED_INCARNATION_SCHEMA.into(), + target: "odin".into(), + plan_id: sha256_id(b"plan"), + incarnation_id: "incarnation-1".into(), + sealed_release_id: sha256_id(b"release"), + source_repository: "github.com/GameCult/Odin".into(), + source_revision: "0".repeat(40), + recipe_sha256: sha256_id(b"recipe"), + runtime_id: "odin-daemon".into(), + expected_signer_identity_id: "odin-provider".into(), + health_contract: "runtime-presence-health-v2".into(), + artifact_sha256: sha256_id(b"artifact"), + state_schema_generation: None, + state_contract_sha256: None, + write_lease_required: false, + route: None, + capabilities: Vec::new(), + dependencies: Vec::new(), + }; + let activation = IdunnRuntimeActivationRecord { + schema_version: cultnet_rs::IDUNN_RUNTIME_ACTIVATION_SCHEMA.into(), + expected_projection_sha256: expected + .canonical_sha256() + .expect("Expected projection digests"), + runtime_id: "odin-daemon".into(), + runtime_instance_id: "instance-1".into(), + activation_signer_identity_id: "odin-activation".into(), + activation_signer_public_key: vec![1; 32], + issued_at_unix_millis: 100, + idunn_signer_identity_id: "idunn".into(), + signature_algorithm: "ed25519".into(), + signature: Vec::new(), + }; + (expected, activation) + } + + fn correlation_for( + expected: &IdunnExpectedIncarnationRecord, + activation: &IdunnRuntimeActivationRecord, + sequence: u64, + ) -> OdinRuntimeTopologyCorrelationRecord { + let mut record = correlation( + &expected.target, + &expected.canonical_sha256().expect("Expected projection digests"), + Some(&activation.runtime_instance_id), + sequence, + ); + record.runtime_id = expected.runtime_id.clone(); + record + } + fn correlation( target: &str, projection_sha256: &str, @@ -6083,53 +6104,42 @@ mod tests { } #[test] - fn a_provider_that_keeps_publishing_after_ready_is_still_ready() -> Result<()> { - // The shape that wedged odin on yggdrasil: ready at sequence 3841, - // latest at 210313, same incarnation, candidate serving. Equality of - // the two statements required Odin to have stopped observing. - let ready = correlation("odin", "projection-a", Some("instance-1"), 3841); - let latest = correlation("odin", "projection-a", Some("instance-1"), 210_313); - same_incarnation(&ready, &latest)?; - latest_is_not_older(&topology(3841, 1), &topology(210_313, 2))?; + fn a_provider_that_keeps_publishing_after_ready_is_admitted() -> Result<()> { + // The shape that wedged odin on yggdrasil: the receipt sat at sequence + // 3841 while the cursor ran to 297765, same incarnation, candidate + // serving. Admission asks about the declared incarnation, so the + // cursor's distance from the receipt is not a question it can fail. + let (expected, activation) = declared_incarnation(); + let observed = correlation_for(&expected, &activation, 297_765); + observation_describes_declared_incarnation(&observed, &expected, &activation)?; Ok(()) } #[test] - fn a_newer_observation_of_another_incarnation_is_refused() -> Result<()> { - let ready = correlation("odin", "projection-a", Some("instance-1"), 3841); + fn an_observation_of_another_incarnation_is_refused() -> Result<()> { + let (expected, activation) = declared_incarnation(); - // Redeployed underneath us: same target, new Expected projection. - let reprojected = correlation("odin", "projection-b", Some("instance-1"), 210_313); - assert!(same_incarnation(&ready, &reprojected).is_err()); + // Restarted underneath us: the projection still matches, the process + // does not. This is the failure the old gate caught by accident and + // the replacement must catch on purpose. + let mut restarted = correlation_for(&expected, &activation, 297_765); + restarted.runtime_instance_id = Some("another-instance".into()); + assert!(observation_describes_declared_incarnation(&restarted, &expected, &activation).is_err()); - // Restarted underneath us: same projection, new running instance. This - // is the case the equality gate genuinely caught and the replacement - // must keep catching. - let restarted = correlation("odin", "projection-a", Some("instance-2"), 210_313); - assert!(same_incarnation(&ready, &restarted).is_err()); + // Redeployed underneath us: a different Expected projection entirely. + let mut reprojected = correlation_for(&expected, &activation, 297_765); + reprojected.expected_projection_sha256 = "sha256-something-else".into(); + assert!(observation_describes_declared_incarnation(&reprojected, &expected, &activation).is_err()); - // A different target's correlation is never this target's evidence. - let stranger = correlation("ghostlight", "projection-a", Some("instance-1"), 210_313); - assert!(same_incarnation(&ready, &stranger).is_err()); + // Another target's correlation is never this target's evidence. + let mut stranger = correlation_for(&expected, &activation, 297_765); + stranger.target = "ghostlight".into(); + assert!(observation_describes_declared_incarnation(&stranger, &expected, &activation).is_err()); // An absent instance id must not read as a match for a present one. - let anonymous = correlation("odin", "projection-a", None, 210_313); - assert!(same_incarnation(&ready, &anonymous).is_err()); - Ok(()) - } - - #[test] - fn an_observation_older_than_the_ready_receipt_is_refused() -> Result<()> { - // Freshness still matters: a replayed older correlation must not be - // able to stand in for the current one. - assert!(latest_is_not_older(&topology(3841, 1), &topology(3840, 2)).is_err()); - // Equal is fine -- a target that has published nothing since going - // ready is the case the old gate handled, and it must keep working. - latest_is_not_older(&topology(3841, 1), &topology(3841, 1))?; - - let mut foreign = topology(210_313, 2); - foreign.signer_identity_id = "someone-else".into(); - assert!(latest_is_not_older(&topology(3841, 1), &foreign).is_err()); + let mut anonymous = correlation_for(&expected, &activation, 297_765); + anonymous.runtime_instance_id = None; + assert!(observation_describes_declared_incarnation(&anonymous, &expected, &activation).is_err()); Ok(()) }