diff --git a/docs/deployment-authority.md b/docs/deployment-authority.md index 0ee3483..da7f028 100644 --- a/docs/deployment-authority.md +++ b/docs/deployment-authority.md @@ -314,9 +314,19 @@ window. A failed continuity restart leaves its own candidate activation projected under the shared Expected key. Its abort demotes that key to Expected-only with the -exact activation the transaction issued, so the projection never drifts from the -admitted receipt. Rollback after fencing demotes the admitted generation's exact -activation. No path adopts an activation the transaction did not issue. +exact activation the transaction issued. Rollback after fencing demotes the +admitted generation's exact activation. No path adopts an activation the +transaction did not issue. + +A projection can still hold an activation the abort never resolved: a failure +between preparing a candidate and publishing its observation, an abort written +by an earlier Idunn, or an operator edit. Continuity therefore restarts a +generation only from an Expected-only projection: when its own demotion is +refused it mints no restart, records the deferral in the generation's +continuity backoff, and tries again after it. At boot, Idunn demotes an +activation still projected for a failed continuity in history, by that +transaction's exact activation, and only reports one no failed transaction +issued. Idunn starts and recovers from its own durable admitted state. Odin is the first managed semantic daemon, never an Idunn bootstrap dependency. Odin's initial diff --git a/src/control_plane.rs b/src/control_plane.rs index cd82a0e..6c97384 100644 --- a/src/control_plane.rs +++ b/src/control_plane.rs @@ -62,6 +62,10 @@ const ODIN_RENDEZVOUS_CAPABILITY: &str = "odin.verse-rendezvous"; /// and few because each attempt holds the target against any deployment. const CONTINUITY_RESTART_ATTEMPTS: usize = 3; +/// How long continuity waits after it could not prepare a restart (the +/// projection would not demote) before trying again. +const CONTINUITY_DEFERRAL_MILLIS: u64 = 30_000; + const DEFAULT_TOPOLOGY_MAXIMUM_AGE_MILLIS: u64 = 30_000; const DEFAULT_TOPOLOGY_MAXIMUM_FUTURE_SKEW_MILLIS: u64 = 2_000; @@ -2693,13 +2697,46 @@ fn read_transaction_record(envelope: &CultCacheEnvelope) -> Result { - let value = lift_legacy_transaction(envelope)?; + let mut value = lift_legacy_transaction(envelope)?; + owe_legacy_continuity_projection(&mut value); value.validate()?; Ok(value) } } } +/// A continuity abort written before the single resolution rule recorded no +/// projection cleanup although its transaction had issued an activation, and +/// so left that activation standing. The current rule owes the demotion, so a +/// resident record of that shape lifts to `Pending`: the one resolution then +/// demotes the transaction's own activation and cleans the residue too. +/// +/// A record already terminal under the old rule is reopened at Starting (a +/// pre-fencing failure is before Fencing by definition) with its completion +/// dropped, because a terminal record cannot owe work. Only the control +/// store's read applies this: history describes what happened and is lifted +/// unchanged. +fn owe_legacy_continuity_projection(transaction: &mut DeploymentTransaction) { + if transaction.command_kind != CommandKind::Continuity || transaction.activation.is_none() { + return; + } + if let Some(abort) = transaction.pre_fencing_abort.as_mut() + && abort.topology_reconciliation == CleanupEvidence::Skipped + { + abort.topology_reconciliation = CleanupEvidence::Pending; + if transaction.completion.take().is_some() { + transaction.phase = DeploymentPhase::Starting; + transaction.phase_deadline = None; + } + } + if let Some(abort) = transaction.post_fencing_abort.as_mut() + && abort.topology_reconciliation == CleanupEvidence::Skipped + && transaction.completion.is_none() + { + abort.topology_reconciliation = CleanupEvidence::Pending; + } +} + /// Read one stored admitted generation, lifting a v2 record to the current /// shape by the same rule as `read_transaction_record`. fn read_generation_record(envelope: &CultCacheEnvelope) -> Result { @@ -2867,41 +2904,92 @@ fn archive_terminal_transaction(state_store: &Path, envelope: &CultCacheEnvelope Ok(()) } -/// Finished transactions, read leniently and for display only. -/// -/// `control.cc` keeps the byte-exact canonical check because its records still -/// gate decisions. History describes what already happened, so an unreadable -/// entry here is skipped rather than allowed to refuse the read -- that -/// asymmetry is the whole reason the two files are separate. -fn read_history_transactions(state_store: &Path) -> Vec { +/// What `history.cc` yielded: the transactions it decoded and the entries it +/// could not, each with its key and reason. +struct HistoryRead { + transactions: Vec, + undecodable: Vec<(String, String)>, +} + +impl HistoryRead { + /// What is missing from `transactions`, or `None` when nothing is. + fn report(&self) -> Option { + if self.undecodable.is_empty() { + return None; + } + let keys = self + .undecodable + .iter() + .map(|(key, error)| format!("{key} ({error})")) + .collect::>() + .join("; "); + Some(format!( + "Idunn history: {} archived transaction(s) could not be decoded and are not counted \ + by status or continuity backoff: {keys}", + self.undecodable.len() + )) + } +} + +/// Finished transactions. `control.cc` keeps the byte-exact canonical check +/// because its records still gate decisions. History describes what already +/// happened, so an undecodable entry is reported and skipped rather than +/// allowed to refuse the read -- that asymmetry is the whole reason the two +/// files are separate. A file that cannot be read at all is an error: the +/// caller decides whether an empty answer is safe. +fn read_history(state_store: &Path) -> Result { let path = history_store_path(state_store); if !path.exists() { - return Vec::new(); + return Ok(HistoryRead { + transactions: Vec::new(), + undecodable: Vec::new(), + }); } - let envelopes = match SingleFileMessagePackBackingStore::new(&path).pull_all_read_only_snapshot() - { - Ok(envelopes) => envelopes, - Err(error) => { - eprintln!( - "Idunn cannot read {}: {error:#}; every archived transaction is invisible \ - to status and to continuity backoff", - path.display() - ); - return Vec::new(); - } - }; + let envelopes = SingleFileMessagePackBackingStore::new(&path) + .pull_all_read_only_snapshot() + .with_context(|| format!("Idunn cannot read {}", path.display()))?; let (transactions, undecodable) = decode_history_transactions(envelopes); - if !undecodable.is_empty() { - for (key, error) in &undecodable { - eprintln!("Idunn history holds an undecodable transaction {key}: {error}"); + Ok(HistoryRead { + transactions, + undecodable, + }) +} + +/// History for display only (status). An unreadable file shows as empty and +/// says so; decisions use `Engine::history_for_decision`, which does not. +fn read_history_transactions(state_store: &Path) -> Vec { + match read_history(state_store) { + Ok(history) => { + if let Some(report) = history.report() { + eprintln!("{report}"); + } + history.transactions + } + Err(error) => { + eprintln!("{error:#}; every archived transaction is invisible to status"); + Vec::new() } - eprintln!( - "Idunn history: {} archived transaction(s) could not be decoded and are not counted \ - by status or continuity backoff", - undecodable.len() - ); } - transactions +} + +/// Says a fault once while it lasts. A scheduler tick that hits the same +/// unreadable file every half second must not print it every half second. +#[derive(Default)] +struct ReportOnce { + last: Mutex>, +} + +impl ReportOnce { + /// The text to print now: `report` when it differs from the last one + /// offered, nothing while the same fault stands or once it has cleared. + fn offer(&self, report: Option) -> Option { + let mut last = self.last.lock().expect("report mutex"); + if *last == report { + return None; + } + last.clone_from(&report); + report + } } /// Decode every archived transaction. An entry that fails is returned with its @@ -3382,6 +3470,7 @@ struct Engine { docker_runner: DockerRunnerDriver, systemd_workload: Arc, host_actuators: Option, + history_report: ReportOnce, } impl Engine { @@ -3436,6 +3525,7 @@ impl Engine { docker_runner: DockerRunnerDriver::default(), systemd_workload, host_actuators, + history_report: ReportOnce::default(), }) } @@ -3512,6 +3602,27 @@ impl Engine { }) } + /// History for a decision that would be wrong on an empty answer: the + /// continuity restart ceiling and the check that a command is consumed. + /// `None` means `history.cc` cannot be read, so the decision is not made; + /// a history that merely has no entries is `Some`. The fault is printed + /// once while it lasts, not on every tick. + fn history_for_decision(&self) -> Option> { + let (report, transactions) = match read_history(&self.options.state_store) { + Ok(history) => (history.report(), Some(history.transactions)), + Err(error) => ( + Some(format!( + "{error:#}; continuity and command retirement stop until it is readable" + )), + None, + ), + }; + if let Some(text) = self.history_report.offer(report) { + eprintln!("{text}"); + } + transactions + } + fn topology(&self) -> CultCacheTopologyDriver { CultCacheTopologyDriver { projection_store: self.options.topology_store.clone(), @@ -3684,6 +3795,96 @@ impl Engine { } } +/// What the boot reconciliation of failed continuity projections found. +#[derive(Debug, Default, PartialEq, Eq)] +struct ProjectionReconciliation { + /// Failed continuity transactions whose exact activation was demoted. + demoted: Vec, + /// Targets whose projection names an activation that neither the admitted + /// generation, a live transaction, nor a failed issuer accounts for. They + /// are reported and left alone. + unexplained: Vec, +} + +impl Engine { + /// Once at boot: demote the activation a failed continuity left standing, + /// where the projection still names exactly the activation that failed + /// transaction issued. A continuity abort before the single resolution + /// rule left such residue (and the lift owes the same demotion to any + /// abort still resident). The issuer is identified by exact activation, + /// from history and from resident terminal records not yet retired to it. + /// + /// Nothing is adopted: an activation with no failed issuer is never + /// demoted or kept on anyone's behalf, only reported. + fn reconcile_failed_continuity_projections(&self) -> Result { + let snapshot = ControlSnapshot::read(&self.options.state_store)?; + let mut outcome = ProjectionReconciliation::default(); + let Some(history) = self.history_for_decision() else { + return Ok(outcome); + }; + let topology = self.topology(); + let issuers = history + .iter() + .chain(snapshot.transactions.iter().map(|stored| &stored.value)) + .filter(|transaction| { + transaction.command_kind == CommandKind::Continuity + && matches!( + transaction.completion, + Some(TransactionCompletion::FailedBeforeFencing { .. }) + | Some(TransactionCompletion::FailedAfterFencing { .. }) + ) + }); + for issuer in issuers { + let (Some(expected), Some(activation), Some(plan)) = + (&issuer.expected, &issuer.activation, &issuer.plan) + else { + continue; + }; + let demoted = (|| -> Result { + if topology.projected_activation(expected)?.as_ref() != Some(activation) { + return Ok(false); + } + let provider_anchor = self.provider_anchor_for_plan(plan)?; + topology.demote_to_expected_only(expected, &provider_anchor, activation, None)?; + Ok(true) + })(); + match demoted { + Ok(true) => outcome.demoted.push(issuer.transaction_id.clone()), + Ok(false) => {} + Err(error) => eprintln!( + "Idunn could not reconcile the projection of failed continuity {}: {error:#}", + issuer.transaction_id + ), + } + } + for generation in &snapshot.admitted { + let projected = match topology.projected_activation(&generation.value.expected) { + Ok(projected) => projected, + Err(error) => { + eprintln!( + "Idunn cannot read the projected activation of {}: {error:#}", + generation.value.target + ); + continue; + } + }; + let Some(projected) = projected else { + continue; + }; + let accounted = projected == generation.value.activation + || snapshot.transactions.iter().any(|stored| { + stored.value.target == generation.value.target + && !stored.value.is_terminal() + && stored.value.activation.as_ref() == Some(&projected) + }); + if !accounted { + outcome.unexplained.push(generation.value.target.clone()); + } + } + Ok(outcome) + } +} + fn serve(options: RuntimeOptions) -> Result<()> { for path in [ &options.state_store, @@ -3710,6 +3911,20 @@ fn serve(options: RuntimeOptions) -> Result<()> { ControlSnapshot::read(&options.state_store).context("validating all Idunn records")?; let engine = Engine::open(options)?; engine.validate_durable_authority(&ControlSnapshot::read(&engine.options.state_store)?)?; + match engine.reconcile_failed_continuity_projections() { + Ok(outcome) => { + for transaction_id in &outcome.demoted { + println!("demoted the activation failed continuity {transaction_id} left projected"); + } + for target in &outcome.unexplained { + eprintln!( + "Idunn found an activation projected for {target} that no admitted generation, \ + live transaction or failed continuity accounts for; left as it is" + ); + } + } + Err(error) => eprintln!("Idunn boot projection reconciliation failed: {error:#}"), + } loop { match engine.run_scheduler_tick() { @@ -3848,7 +4063,12 @@ impl Engine { // whose transactions were retired to history before commands travelled // with them are still resident; they are not queued, they are // history that never moved. Retire them here and never freeze them. - let historical_commands = read_history_transactions(&self.options.state_store) + // With history unreadable, whether a command was consumed is unknown: + // freezing it again could run a finished deployment twice. + let Some(history) = self.history_for_decision() else { + return Ok(false); + }; + let historical_commands = history .into_iter() .map(|transaction| transaction.command_id) .collect::>(); @@ -4165,12 +4385,17 @@ impl Engine { | Some(TransactionCompletion::FailedAfterFencing { .. }) ) }; + // An unreadable history counts as zero refusals only by lying: + // the ceiling would never trip. Without it, nothing is actuated. + let Some(history) = self.history_for_decision() else { + continue; + }; let refused_restarts = snapshot .transactions .iter() .filter(|stored| is_refused_restart(&stored.value)) .count() - + read_history_transactions(&self.options.state_store) + + history .iter() .filter(|transaction| is_refused_restart(transaction)) .count(); @@ -4218,6 +4443,17 @@ impl Engine { // Demote to Expected-only, the same shape an aborted deployment // leaves, and let the restart publish its own activation once it is // observed. + let now = now_millis()?; + // A restart whose projection could not be prepared is deferred in + // the generation's own backoff, not retried every tick. + if current + .value + .continuity_backoff + .next_restart_at_unix_millis + .is_some_and(|not_before| now < not_before) + { + continue; + } let demotion = (|| -> Result<()> { let topology = self.topology(); let provider_anchor = self.provider_anchor_for_plan(¤t.value.plan)?; @@ -4232,13 +4468,21 @@ impl Engine { )?; Ok(()) })(); + // Expected-only is the precondition of a restart: a continuity + // minted over a projection that still names an activation would + // fail between preparing and publishing its own, and its abort + // could not resolve. So a failed demotion mints nothing. The + // failure is written to the generation, and the next attempt is + // not before the deferral ends. if let Err(error) = demotion { eprintln!( - "Idunn preserved admitted {} after refusing to demote its projection: {error:#}", + "Idunn deferred continuity for admitted {} after refusing to demote its projection: {error:#}", current.value.target ); + self.defer_continuity_restart(current, now)?; + progressed = true; + continue; } - let now = now_millis()?; // An engaged lifecycle brake means no continuity transaction is // minted at all. A transaction that exists and waits on the brake // owns the target, and a target owned by a parked restart accepts @@ -4286,6 +4530,29 @@ impl Engine { Ok(progressed) } + /// Record that continuity may not restart this generation before + /// `now + CONTINUITY_DEFERRAL_MILLIS`. The generation's `ContinuityBackoff` + /// owns "not before": only its `next_restart_at_unix_millis` is written, + /// so the attempts and window that count real restarts are untouched. + fn defer_continuity_restart(&self, current: &Stored, now: u64) -> Result<()> { + let mut next = current.value.clone(); + next.continuity_backoff.next_restart_at_unix_millis = + Some(now + CONTINUITY_DEFERRAL_MILLIS); + next.validate()?; + ensure!( + SingleFileMessagePackBackingStore::new(&self.options.state_store).compare_exchange( + &[CultCacheExpectedEnvelope { + r#type: AdmittedGeneration::TYPE.into(), + key: current.value.target.clone(), + current: Some(current.envelope.clone()), + }], + &[admitted_envelope(&next, now)?], + )?, + "admitted generation changed before continuity deferral CAS" + ); + Ok(()) + } + fn supervise_admitted_route(&self, current: &Stored) -> Result { let Some(expected_route) = current.value.expected.route.as_ref() else { ensure!( @@ -9541,25 +9808,107 @@ mod tests { } } - /// A Continuity transaction for an unrouted, stateless target, sitting at - /// `phase` (Warming or later) with every earlier phase's evidence in place. The plan, sealed - /// release, Expected, activation and Warming evidence are the real - /// constructions; only the workload and isolation observations are - /// borrowed from a recorded transaction. Continuity is chosen because a - /// Deploy would need a frozen source and a signed brake record, which no - /// phase past Fencing reads. + /// A Continuity transaction for an unrouted, stateless target with no + /// incumbent: the shape most Engine tests need. fn transaction_at( world: &EngineFixture, phase: DeploymentPhase, + ) -> Result { + seeded_transaction(world, phase, CommandKind::Continuity, None) + } + + /// The Odin publisher sequence a transaction seeded over `incumbent` + /// warms at; its Ready is the next one. Above anything an earlier + /// generation of the world admitted. + fn seeded_sequence(incumbent: Option<&AdmittedGeneration>) -> u64 { + if incumbent.is_some() { 14 } else { 4 } + } + + /// A released deployment brake receipt, signed by an operator identity + /// enrolled in the world: what a Deploy's `deployment_authorization` holds. + fn deployment_authorization( + world: &EngineFixture, + expected: &IdunnExpectedIncarnationRecord, + transaction_id: &str, + now: u64, + ) -> Result { + use cultnet_rs::{ + IDUNN_DEPLOYMENT_BRAKE_AUTHORITY, IDUNN_DEPLOYMENT_BRAKE_ID, + IDUNN_DEPLOYMENT_BRAKE_SCOPE, IDUNN_DEPLOYMENT_RELEASE_PURPOSE, + IdunnDeploymentBrakeReleasePurpose, enroll_service_identity_at, + }; + let signer = enroll_service_identity_at::( + &world.root.join("identities/brake-operator.cc"), + )?; + export_service_identity_trust_anchor(&signer, &world.root.join("brake-anchor.cc"))?; + let mut record = IdunnDeploymentBrakeRecord { + schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(), + brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(), + authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(), + runtime_id: expected.runtime_id.clone(), + status: "released".into(), + scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(), + reason: "test rollout".into(), + observed_at_unix_millis: now, + expires_at_unix_millis: Some(now + 600_000), + authorization_id: Some("authorization-test".into()), + authorization_purpose: Some(IDUNN_DEPLOYMENT_RELEASE_PURPOSE.into()), + authorized_release_id: Some(expected.sealed_release_id.clone()), + authorized_deployment_id: Some(transaction_id.to_owned()), + authorized_by: Some(signer.trust_anchor()?.identity_id), + authorization_issued_at_unix_millis: Some(now), + authorization_expires_at_unix_millis: Some(now + 600_000), + signature_algorithm: Some("ed25519".into()), + signature: None, + private_state_exposed: false, + updated_by: "operator/test".into(), + }; + record.signature = Some( + signer + .sign::(&rmp_serde::to_vec(&record)?) + .signature, + ); + record.validate()?; + let canonical_brake_bytes = rmp_serde::to_vec(&record)?; + Ok(DeploymentAuthorization { + authorization_id: "authorization-test".into(), + brake_sha256: sha256_id(&canonical_brake_bytes), + canonical_brake_bytes, + authorized_at_unix_millis: now, + }) + } + + /// A transaction for an unrouted, stateless target, sitting at `phase` + /// (Warming or later) with every earlier phase's evidence in place. The + /// plan, sealed release, Expected, activation and Warming evidence are the + /// real constructions; only the workload and isolation observations are + /// borrowed from a recorded transaction. + /// + /// `incumbent` is an admitted generation already in the world's store. A + /// Continuity over it restarts that generation's own release, so it shares + /// the incumbent's Expected key. A Deploy over it builds a second + /// incarnation. A Deploy carries what phases past Fencing never read but + /// validation requires: a three-field frozen-source receipt and a + /// well-formed release receipt from the brake. + fn seeded_transaction( + world: &EngineFixture, + phase: DeploymentPhase, + kind: CommandKind, + incumbent: Option<&AdmittedGeneration>, ) -> Result { use crate::deployment_plan::tests::{ BINDING, RECIPE, artifact_receipt, external_input_receipt, source, }; - let provider = enroll_service_identity_at::( - &world.root.join("identities/provider.cc"), - )?; + let provider_path = world.root.join("identities/provider.cc"); let provider_anchor = world.root.join("identities/provider-anchor.cc"); - export_service_identity_trust_anchor(&provider, &provider_anchor)?; + let provider = if provider_path.exists() { + open_service_identity_at::(&provider_path)? + } else { + let provider = + enroll_service_identity_at::(&provider_path)?; + export_service_identity_trust_anchor(&provider, &provider_anchor)?; + provider + }; let (binding_head, binding_tail) = BINDING .split_once("[route]") @@ -9585,34 +9934,60 @@ mod tests { r#"["GAMECULT_IDUNN_CANDIDATE_BIND", "GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#, r#"["GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#, ); - let plan = compile_deployment_plan( - recipe.as_bytes(), - binding.as_bytes(), - source(&recipe), - "service-incarnation-1", - None, - 110, - &[], - )?; - let release = SealedRelease::new( - &plan, - vec![artifact_receipt()], - vec![external_input_receipt()], - 120, - )?; + let (plan, release) = match (kind, incumbent) { + (CommandKind::Continuity, Some(incumbent)) => ( + incumbent.plan.clone(), + incumbent.sealed_release.clone(), + ), + _ => { + let incarnation = if incumbent.is_some() { + "service-incarnation-2" + } else { + "service-incarnation-1" + }; + let plan = compile_deployment_plan( + recipe.as_bytes(), + binding.as_bytes(), + source(&recipe), + incarnation, + None, + 110, + &[], + )?; + let release = SealedRelease::new( + &plan, + vec![artifact_receipt()], + vec![external_input_receipt()], + 120, + )?; + (plan, release) + } + }; let expected = release.expected_projection(&plan)?; assert!(!expected.write_lease_required && expected.route.is_none()); let now = now_millis()?; let command = DeploymentCommand { schema_version: DEPLOYMENT_COMMAND_SCHEMA.into(), - command_id: "continuity-service".into(), - kind: CommandKind::Continuity, + command_id: format!( + "{}-service{}", + match kind { + CommandKind::Deploy => "up", + CommandKind::Continuity => "continuity", + }, + if incumbent.is_some() { "-again" } else { "" } + ), + kind, selector: "service".into(), requested_by: "test".into(), requested_at_unix_millis: 100, }; - let mut transaction = DeploymentTransaction::new(&command, "service".into(), 0, None, now)?; + let mut transaction = match (kind, incumbent) { + (CommandKind::Continuity, Some(incumbent)) => { + DeploymentTransaction::from_continuity(&command, incumbent, now)? + } + _ => DeploymentTransaction::new(&command, "service".into(), 0, incumbent, now)?, + }; let activation = IdunnRuntimeActivationLaunch::issue( &expected, runtime_instance_id(&transaction.transaction_id)?, @@ -9625,12 +10000,30 @@ mod tests { .1 .workload .context("recorded transaction has no workload")?; - transaction.lifecycle_authorized_at_unix_millis = Some(now); + match kind { + CommandKind::Continuity => { + transaction.lifecycle_authorized_at_unix_millis = Some(now); + } + CommandKind::Deploy => { + transaction.frozen_source = Some(FrozenSourceReceipt { + transaction_id: transaction.transaction_id.clone(), + plan_id: plan.plan_id.clone(), + snapshot_sha256: sha256_id(b"frozen source snapshot"), + }); + transaction.deployment_authorization = Some(deployment_authorization( + world, + &expected, + &transaction.transaction_id, + now, + )?); + } + } transaction.expected_publication_sha256 = Some(expected.canonical_sha256()?); transaction.activation_publication_sha256 = Some(activation.canonical_sha256()?); match &mut workload { WorkloadObservation::Systemd(observed) => { observed.runtime_instance_id = activation.runtime_instance_id.clone(); + observed.executable_sha256 = expected.artifact_sha256.clone(); } WorkloadObservation::Host(_) => bail!("recorded workload is not a systemd unit"), } @@ -9646,7 +10039,8 @@ mod tests { transaction.workload = Some(workload); transaction.plan = Some(plan); // Odin's first word about the candidate, before it was Ready. - let warming = signed_correlation(world, &transaction, 4, false)?; + let sequence = seeded_sequence(incumbent); + let warming = signed_correlation(world, &transaction, sequence, false)?; let authenticated = world.engine.authenticate_topology_bytes( &ControlSnapshot::read(&world.state_store)?, &transaction, @@ -9657,7 +10051,7 @@ mod tests { transaction.warming = Some(WarmingEvidence::OdinTopology { evidence: TopologyEvidence::from_authenticated(&authenticated, now)?, }); - transaction.odin_publisher_sequence_cursor = 4; + transaction.odin_publisher_sequence_cursor = sequence; transaction.enter_phase(phase, now); transaction.validate()?; @@ -10564,4 +10958,922 @@ mod tests { assert_eq!(std::fs::read(&world.topology.projection_store)?, before); Ok(()) } + + // --------------------------------------------------------------------- + // B1 fix batch: the transition, the precondition, the boot reconciliation, + // history that fails closed, and the abort paths through the Engine. + // --------------------------------------------------------------------- + + /// A workload port whose process can be killed: observing it succeeds + /// until `kill`, then fails the way a dead unit does. Everything else is + /// `StillWorkload`'s. + struct SwitchWorkload { + alive: std::sync::atomic::AtomicBool, + } + + impl SwitchWorkload { + fn new() -> Arc { + Arc::new(Self { + alive: std::sync::atomic::AtomicBool::new(true), + }) + } + + fn kill(&self) { + self.alive + .store(false, std::sync::atomic::Ordering::SeqCst); + } + } + + impl WorkloadPort for SwitchWorkload { + fn install( + &self, + plan: &CompiledDeploymentPlan, + release: &crate::drivers::MaterializedRelease, + ) -> Result { + StillWorkload.install(plan, release) + } + fn prepare_activation( + &self, + plan: &CompiledDeploymentPlan, + expected: &IdunnExpectedIncarnationRecord, + launch: IdunnRuntimeActivationLaunch, + ) -> Result { + StillWorkload.prepare_activation(plan, expected, launch) + } + fn start_prepared( + &self, + plan: &CompiledDeploymentPlan, + release: &SealedRelease, + installed: &crate::drivers::InstalledReleaseObservation, + expected: &IdunnExpectedIncarnationRecord, + activation: &IdunnRuntimeActivationRecord, + ) -> Result { + StillWorkload.start_prepared(plan, release, installed, expected, activation) + } + fn discard_prepared( + &self, + _: &CompiledDeploymentPlan, + _: &IdunnExpectedIncarnationRecord, + _: &IdunnRuntimeActivationRecord, + ) -> Result<()> { + Ok(()) + } + fn observe( + &self, + _: &IdunnExpectedIncarnationRecord, + _: &IdunnRuntimeActivationRecord, + prior: &WorkloadObservation, + ) -> Result { + ensure!( + self.alive.load(std::sync::atomic::Ordering::SeqCst), + "the workload is gone" + ); + Ok(prior.clone()) + } + fn stop(&self, _: &WorkloadObservation) -> Result<()> { + Ok(()) + } + fn is_permanently_stopped(&self, _: &WorkloadObservation) -> Result { + Ok(false) + } + } + + /// Admit a generation for "service" through the real phase machine and + /// retire its transaction, leaving the world with an incumbent. + fn admit_incumbent(world: &EngineFixture) -> Result { + let seeded = transaction_at(world, DeploymentPhase::Fencing)?; + odin_reports_ready(world, &seeded, seeded_sequence(None) + 1)?; + drive(world, |transaction| { + transaction.phase == DeploymentPhase::Complete + })?; + assert!(world.engine.retire_one_terminal_transaction()?); + Ok(ControlSnapshot::read(&world.state_store)? + .admitted_for("service") + .context("commit wrote no admitted generation")? + .value + .clone()) + } + + /// Change the incumbent in the store, compare-and-swap. + fn edit_incumbent( + world: &EngineFixture, + edit: impl FnOnce(&mut AdmittedGeneration), + ) -> Result { + let snapshot = ControlSnapshot::read(&world.state_store)?; + let stored = snapshot + .admitted_for("service") + .context("no admitted generation")?; + let mut next = stored.value.clone(); + edit(&mut next); + assert!( + SingleFileMessagePackBackingStore::new(&world.state_store).compare_exchange( + &[CultCacheExpectedEnvelope { + r#type: AdmittedGeneration::TYPE.into(), + key: "service".into(), + current: Some(stored.envelope.clone()), + }], + &[admitted_envelope(&next, now_millis()?)?], + )? + ); + Ok(next) + } + + fn incumbent_envelope(world: &EngineFixture) -> Result { + Ok(ControlSnapshot::read(&world.state_store)? + .admitted_for("service") + .context("no admitted generation")? + .envelope + .clone()) + } + + /// Publish what a transaction's candidate projects: its Expected and, + /// when asked, its observed activation. + fn project_candidate( + world: &EngineFixture, + transaction: &DeploymentTransaction, + with_activation: bool, + ) -> Result<()> { + let expected = transaction.expected.as_ref().context("no Expected")?; + let anchor = world + .engine + .provider_anchor_for_plan(transaction.plan.as_ref().context("no plan")?)?; + let topology = world.engine.topology(); + topology.publish_expected(expected, &anchor)?; + if with_activation { + // The observation must name the release the Expected declares; + // the borrowed workload is patched to. + let mut workload = transaction.workload.clone().context("no workload")?; + if let WorkloadObservation::Systemd(observed) = &mut workload { + observed.executable_sha256 = expected.artifact_sha256.clone(); + } + topology.publish_observed_activation( + expected, + transaction.activation.as_ref().context("no activation")?, + &workload, + )?; + } + Ok(()) + } + + /// Publish what an admitted generation projects while it runs. + fn project_admitted(world: &EngineFixture, generation: &AdmittedGeneration) -> Result<()> { + let anchor = world.engine.provider_anchor_for_plan(&generation.plan)?; + let topology = world.engine.topology(); + topology.publish_expected(&generation.expected, &anchor)?; + topology.publish_observed_activation( + &generation.expected, + &generation.activation, + &generation.workload, + )?; + Ok(()) + } + + /// The types of the records projected under one incarnation key, sorted. + fn projected_under( + world: &EngineFixture, + expected: &IdunnExpectedIncarnationRecord, + ) -> Result> { + let path = &world.engine.options.topology_store; + if !path.exists() { + return Ok(Vec::new()); + } + let key = incarnation_key(expected)?; + let mut types = SingleFileMessagePackBackingStore::new(path) + .pull_all_read_only_snapshot()? + .into_iter() + .filter(|entry| entry.key == key) + .map(|entry| entry.r#type) + .collect::>(); + types.sort(); + Ok(types) + } + + /// One transaction by id, wherever it now lives. A failed transaction is + /// archived the moment it finishes, so it is in history, not resident. + fn record_of(world: &EngineFixture, transaction_id: &str) -> Result { + if let Some(stored) = ControlSnapshot::read(&world.state_store)? + .transactions + .into_iter() + .find(|stored| stored.value.transaction_id == transaction_id) + { + return Ok(stored.value); + } + read_history_transactions(&world.state_store) + .into_iter() + .find(|transaction| transaction.transaction_id == transaction_id) + .context("the transaction is in neither the live set nor history") + } + + fn expected_only() -> Vec { + vec![IdunnExpectedIncarnationRecord::TYPE.to_owned()] + } + + // ---- the Engine's abort paths, Deploy and incumbent ---- + + #[test] + fn a_deploy_that_fences_commits_and_retires_the_incumbent_it_replaced() -> Result<()> { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + admit_incumbent(&world)?; + let backoff = ContinuityBackoff { + window_started_at_unix_millis: Some(1_000), + attempts: 2, + next_restart_at_unix_millis: Some(9_000), + }; + let incumbent = edit_incumbent(&world, |generation| { + generation.continuity_backoff = backoff.clone(); + })?; + + let candidate = seeded_transaction( + &world, + DeploymentPhase::Fencing, + CommandKind::Deploy, + Some(&incumbent), + )?; + odin_reports_ready(&world, &candidate, seeded_sequence(Some(&incumbent)) + 1)?; + drive(&world, |transaction| { + transaction.phase == DeploymentPhase::Complete + })?; + + let admitted = ControlSnapshot::read(&world.state_store)? + .admitted_for("service") + .context("the deploy admitted nothing")? + .value + .clone(); + assert_eq!(admitted.transaction_id, candidate.transaction_id); + assert_ne!(admitted.generation_id, incumbent.generation_id); + // The restart ceiling belongs to the target: a commit must not reset it. + assert_eq!(admitted.continuity_backoff, backoff); + assert_eq!(admitted.route_supervision, incumbent.route_supervision); + + // The commit did not stop the incumbent: it is retired after. + let committed = record_of(&world, &candidate.transaction_id)?; + let cleanup = committed.post_commit_cleanup.clone().context("no cleanup")?; + assert_eq!( + cleanup.incumbent, + IncumbentCleanupEvidence::Pending { + generation_id: incumbent.generation_id.clone(), + workload: incumbent.workload.clone(), + } + ); + assert_eq!(cleanup.source, SourceCleanupEvidence::Pending); + let frozen = world + .engine + .options + .staging_root + .join("frozen-sources") + .join(&candidate.transaction_id); + std::fs::create_dir_all(&frozen)?; + std::fs::write(frozen.join("tree"), b"frozen")?; + + drive(&world, |transaction| transaction.is_terminal())?; + let retired = record_of(&world, &candidate.transaction_id)? + .post_commit_cleanup + .context("no cleanup")?; + assert_eq!( + retired.incumbent, + IncumbentCleanupEvidence::Complete { + generation_id: incumbent.generation_id + } + ); + assert_eq!(retired.source, SourceCleanupEvidence::Complete); + assert!(!frozen.exists(), "the frozen source outlived its deploy"); + Ok(()) + } + + #[test] + fn a_deploy_post_fence_abort_cleans_up_and_restores_its_incumbent_exactly() -> Result<()> { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let incumbent = admit_incumbent(&world)?; + project_admitted(&world, &incumbent)?; + let candidate = seeded_transaction( + &world, + DeploymentPhase::Fencing, + CommandKind::Deploy, + Some(&incumbent), + )?; + project_candidate(&world, &candidate, true)?; + let candidate_expected = candidate.expected.clone().context("no Expected")?; + assert_eq!( + projected_under(&world, &candidate_expected)?.len(), + 2, + "the candidate did not project" + ); + let frozen = world + .engine + .options + .staging_root + .join("frozen-sources") + .join(&candidate.transaction_id); + std::fs::create_dir_all(&frozen)?; + std::fs::write(frozen.join("tree"), b"frozen")?; + let before = incumbent_envelope(&world)?; + + world + .engine + .begin_post_fencing_abort(&resident(&world)?, anyhow!("candidate refused"))?; + assert!(resident(&world)?.value.post_fencing_abort.is_some()); + drive(&world, |transaction| transaction.completion.is_some())?; + + let finished = record_of(&world, &candidate.transaction_id)?; + assert!(matches!( + finished.completion, + Some(TransactionCompletion::FailedAfterFencing { + recovery: TerminalRecovery::RestoreIncumbent, + .. + }) + )); + let abort = finished.post_fencing_abort.context("no abort evidence")?; + assert_eq!(abort.topology_reconciliation, CleanupEvidence::Complete); + assert_eq!(abort.source_cleanup, CleanupEvidence::Complete); + assert!(!frozen.exists(), "the abandoned source was not cleaned"); + assert!( + projected_under(&world, &candidate_expected)?.is_empty(), + "the abandoned candidate is still projected" + ); + // Fencing stopped the incumbent: it stands as Expected-only, with the + // exact activation it ran under withdrawn, ready for continuity. + assert_eq!(projected_under(&world, &incumbent.expected)?, expected_only()); + assert_eq!(incumbent_envelope(&world)?, before); + Ok(()) + } + + #[test] + fn a_deploy_pre_fence_abort_cleans_up_and_leaves_its_incumbent_running() -> Result<()> { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let incumbent = admit_incumbent(&world)?; + project_admitted(&world, &incumbent)?; + let candidate = seeded_transaction( + &world, + DeploymentPhase::Warming, + CommandKind::Deploy, + Some(&incumbent), + )?; + project_candidate(&world, &candidate, true)?; + let candidate_expected = candidate.expected.clone().context("no Expected")?; + let frozen = world + .engine + .options + .staging_root + .join("frozen-sources") + .join(&candidate.transaction_id); + std::fs::create_dir_all(&frozen)?; + std::fs::write(frozen.join("tree"), b"frozen")?; + let before = incumbent_envelope(&world)?; + + world + .engine + .begin_pre_fencing_abort(&resident(&world)?, anyhow!("candidate refused"))?; + let intent = resident(&world)?.value.pre_fencing_abort.context("no intent")?; + assert_eq!(intent.topology_reconciliation, CleanupEvidence::Pending); + assert_eq!(intent.source_cleanup, CleanupEvidence::Pending); + drive(&world, |transaction| transaction.completion.is_some())?; + + let finished = record_of(&world, &candidate.transaction_id)?; + assert!(matches!( + finished.completion, + Some(TransactionCompletion::FailedBeforeFencing { .. }) + )); + assert!(finished.is_terminal()); + assert!(!frozen.exists(), "the abandoned source was not cleaned"); + assert!(projected_under(&world, &candidate_expected)?.is_empty()); + // Before fencing a deployment never touched its incumbent. + assert_eq!( + projected_under(&world, &incumbent.expected)?, + { + let mut types = vec![ + IdunnExpectedIncarnationRecord::TYPE.to_owned(), + IdunnRuntimeActivationRecord::TYPE.to_owned(), + ]; + types.sort(); + types + } + ); + assert_eq!(incumbent_envelope(&world)?, before); + Ok(()) + } + + #[test] + fn a_continuity_abort_over_its_incumbent_demotes_only_the_activation_it_issued() -> Result<()> { + for post_fence in [false, true] { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let incumbent = admit_incumbent(&world)?; + let candidate = seeded_transaction( + &world, + if post_fence { + DeploymentPhase::Fencing + } else { + DeploymentPhase::Warming + }, + CommandKind::Continuity, + Some(&incumbent), + )?; + // The candidate shares the incumbent's key: same Expected, its own + // activation. + assert_eq!(candidate.expected.as_ref(), Some(&incumbent.expected)); + assert_ne!(candidate.activation.as_ref(), Some(&incumbent.activation)); + project_candidate(&world, &candidate, true)?; + let before = incumbent_envelope(&world)?; + + let stored = resident(&world)?; + if post_fence { + world + .engine + .begin_post_fencing_abort(&stored, anyhow!("candidate died"))?; + assert_eq!( + resident(&world)? + .value + .post_fencing_abort + .context("no intent")? + .topology_reconciliation, + CleanupEvidence::Pending + ); + } else { + world + .engine + .begin_pre_fencing_abort(&stored, anyhow!("candidate died"))?; + assert_eq!( + resident(&world)? + .value + .pre_fencing_abort + .context("no intent")? + .topology_reconciliation, + CleanupEvidence::Pending + ); + } + drive(&world, |transaction| transaction.completion.is_some())?; + + assert_eq!( + projected_under(&world, &incumbent.expected)?, + expected_only(), + "post_fence={post_fence}: the shared key must keep its Expected and nothing else" + ); + let finished = record_of(&world, &candidate.transaction_id)?; + assert!(finished.is_terminal()); + assert!(matches!( + finished.completion, + Some( + TransactionCompletion::FailedBeforeFencing { .. } + | TransactionCompletion::FailedAfterFencing { .. } + ) + )); + assert_eq!(incumbent_envelope(&world)?, before); + } + Ok(()) + } + + // ---- fix 1: the legacy lift owns the transition ---- + + const FIXTURE_PRE_B1_ABORTS: [(&str, &str); 3] = [ + ( + "pre-fence abort in flight", + include_str!("../tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort.hex"), + ), + ( + "pre-fence abort already terminal", + include_str!( + "../tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort-terminal.hex" + ), + ), + ( + "post-fence abort in flight", + include_str!("../tests/fixtures/idunn-control-legacy/transaction-post-fence-abort.hex"), + ), + ]; + const FIXTURE_PROVIDER_ANCHOR: &str = + include_str!("../tests/fixtures/idunn-control-legacy/provider-anchor.hex"); + /// Where the fixtures' plan binding names the provider trust anchor. + const FIXTURE_ANCHOR_PATH: &str = "/tmp/idunn-b1-legacy/provider-anchor.cc"; + + fn hex_bytes(text: &str) -> Vec { + let hex = text.trim(); + (0..hex.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).expect("hex fixture")) + .collect() + } + + fn write_fixture_anchor() -> Result<()> { + let path = Path::new(FIXTURE_ANCHOR_PATH); + std::fs::create_dir_all(path.parent().context("anchor path has no parent")?)?; + std::fs::write(path, hex_bytes(FIXTURE_PROVIDER_ANCHOR))?; + Ok(()) + } + + #[test] + fn a_pre_b1_continuity_abort_lifts_owing_its_own_activation_and_resolves_it() -> Result<()> { + write_fixture_anchor()?; + for (name, text) in FIXTURE_PRE_B1_ABORTS { + let envelope = fixture_envelope(text, DeploymentTransaction::TYPE)?; + + // The record is what the old rule wrote: an activation was issued + // and the abort says it owes the projection nothing. + let legacy: LegacyDeploymentTransaction = rmp_serde::from_slice(&envelope.payload)?; + assert_eq!(legacy.command_kind, CommandKind::Continuity, "{name}"); + assert!(legacy.activation.is_some(), "{name}"); + let owed = legacy + .pre_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + .or_else(|| { + legacy + .post_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + }) + .context("no abort")?; + assert_eq!(owed, CleanupEvidence::Skipped, "{name}"); + + // History describes what happened, so it lifts the record as written. + let archived = lift_legacy_transaction(&envelope)?; + let archived_owed = archived + .pre_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + .or_else(|| { + archived + .post_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + }) + .context("no abort")?; + assert_eq!(archived_owed, CleanupEvidence::Skipped, "{name}"); + + // The control store lifts it owing the demotion, and validates. + let lifted = read_transaction_record(&envelope)?; + let lifted_owed = lifted + .pre_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + .or_else(|| { + lifted + .post_fencing_abort + .as_ref() + .map(|abort| abort.topology_reconciliation) + }) + .context("no abort")?; + assert_eq!(lifted_owed, CleanupEvidence::Pending, "{name}"); + assert!(!lifted.is_terminal(), "{name}: a record that owes work is terminal"); + assert_eq!(lifted.completion, None, "{name}"); + + // Boot: the store reads, and the abort resolves. + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let command = DeploymentCommand { + schema_version: DEPLOYMENT_COMMAND_SCHEMA.into(), + command_id: lifted.command_id.clone(), + kind: CommandKind::Continuity, + selector: "service".into(), + requested_by: "test".into(), + requested_at_unix_millis: 100, + }; + assert!( + SingleFileMessagePackBackingStore::new(&world.state_store).compare_exchange( + &[ + CultCacheExpectedEnvelope { + r#type: DeploymentCommand::TYPE.into(), + key: command.command_id.clone(), + current: None, + }, + CultCacheExpectedEnvelope { + r#type: DeploymentTransaction::TYPE.into(), + key: envelope.key.clone(), + current: None, + }, + ], + &[command_envelope(&command, 100)?, envelope.clone()], + )?, + "{name}" + ); + assert_eq!(migrate_control_store_to_current_schema(&world.state_store)?, 1); + let snapshot = ControlSnapshot::read(&world.state_store)?; + assert_eq!(snapshot.transactions.len(), 1, "{name}"); + project_candidate(&world, &lifted, true)?; + let expected = lifted.expected.clone().context("no Expected")?; + assert_eq!(projected_under(&world, &expected)?.len(), 2, "{name}"); + + for _ in 0..12 { + let Ok(current) = resident(&world) else { break }; + if current.value.completion.is_some() { + break; + } + world.engine.advance_transaction(¤t)?; + } + let finished = record_of(&world, &envelope.key)?; + assert!(finished.completion.is_some(), "{name}: the abort never resolved"); + assert_eq!( + projected_under(&world, &expected)?, + expected_only(), + "{name}: the projection was not demoted to Expected-only" + ); + } + Ok(()) + } + + // ---- fix 2: supervision owns the Expected-only precondition ---- + + #[test] + fn continuity_mints_no_restart_over_a_projection_it_cannot_demote() -> Result<()> { + let workload = SwitchWorkload::new(); + let world = EngineFixture::with_workload(workload.clone())?; + let incumbent = admit_incumbent(&world)?; + assert_eq!(incumbent.continuity_backoff, ContinuityBackoff::default()); + + // The projection names an activation that is not the incumbent's. + let anchor = world.engine.provider_anchor_for_plan(&incumbent.plan)?; + let topology = world.engine.topology(); + topology.publish_expected(&incumbent.expected, &anchor)?; + let stranger = seeded_transaction( + &world, + DeploymentPhase::Warming, + CommandKind::Continuity, + Some(&incumbent), + )?; + topology.publish_observed_activation( + &incumbent.expected, + stranger.activation.as_ref().context("no activation")?, + stranger.workload.as_ref().context("no workload")?, + )?; + // Remove the seeded stranger transaction: only the projection matters. + let seeded = resident(&world)?; + assert!( + SingleFileMessagePackBackingStore::new(&world.state_store).delete_batch_if_unchanged( + &[ + seeded.envelope.clone(), + ControlSnapshot::read(&world.state_store)? + .commands + .into_iter() + .find(|stored| stored.value.command_id == seeded.value.command_id) + .context("seeded command")? + .envelope, + ] + )? + ); + workload.kill(); + + let before = now_millis()?; + assert!(world.engine.supervise_one_admitted_generation()?); + let snapshot = ControlSnapshot::read(&world.state_store)?; + assert!( + snapshot.transactions.is_empty(), + "continuity minted a restart over a projection it could not demote" + ); + let deferred = snapshot + .admitted_for("service") + .context("no generation")? + .value + .continuity_backoff + .next_restart_at_unix_millis + .context("the failed demotion was not recorded")?; + assert!(deferred >= before + CONTINUITY_DEFERRAL_MILLIS); + + // Deferred: the next tick neither retries nor writes. + let envelope = incumbent_envelope(&world)?; + assert!(!world.engine.supervise_one_admitted_generation()?); + assert_eq!(incumbent_envelope(&world)?, envelope); + assert!(ControlSnapshot::read(&world.state_store)?.transactions.is_empty()); + + // The projection is repaired and the deferral has run out: the restart + // proceeds, from an Expected-only projection. + topology.withdraw_stale_incarnation(&incumbent.expected, &anchor)?; + project_admitted(&world, &incumbent)?; + edit_incumbent(&world, |generation| { + generation.continuity_backoff.next_restart_at_unix_millis = Some(1); + })?; + assert!(world.engine.supervise_one_admitted_generation()?); + let snapshot = ControlSnapshot::read(&world.state_store)?; + assert_eq!(snapshot.transactions.len(), 1); + assert_eq!(snapshot.transactions[0].value.command_kind, CommandKind::Continuity); + assert_eq!(projected_under(&world, &incumbent.expected)?, expected_only()); + Ok(()) + } + + // ---- fix 3: one-time boot reconciliation, by exact issuer ---- + + #[test] + fn boot_demotes_the_activation_a_failed_continuity_left_and_only_that() -> Result<()> { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let incumbent = admit_incumbent(&world)?; + let failed = seeded_transaction( + &world, + DeploymentPhase::Warming, + CommandKind::Continuity, + Some(&incumbent), + )?; + project_candidate(&world, &failed, true)?; + world + .engine + .begin_pre_fencing_abort(&resident(&world)?, anyhow!("candidate died"))?; + drive(&world, |transaction| transaction.completion.is_some())?; + // A failed transaction is archived the moment it finishes. + let aborted = record_of(&world, &failed.transaction_id)?; + assert!(aborted.is_terminal()); + assert!(ControlSnapshot::read(&world.state_store)?.transactions.is_empty()); + assert_eq!(projected_under(&world, &incumbent.expected)?, expected_only()); + + // Drift a pre-B1 abort left: the failed candidate's activation stands. + project_candidate(&world, &failed, true)?; + assert_eq!(projected_under(&world, &incumbent.expected)?.len(), 2); + + let outcome = world.engine.reconcile_failed_continuity_projections()?; + assert_eq!( + outcome, + ProjectionReconciliation { + demoted: vec![failed.transaction_id.clone()], + unexplained: Vec::new(), + } + ); + assert_eq!(projected_under(&world, &incumbent.expected)?, expected_only()); + + // Once: nothing is left to demote. + assert_eq!( + world.engine.reconcile_failed_continuity_projections()?, + ProjectionReconciliation::default() + ); + Ok(()) + } + + #[test] + fn boot_never_adopts_an_activation_no_failed_transaction_issued() -> Result<()> { + let world = EngineFixture::with_workload(Arc::new(StillWorkload))?; + let incumbent = admit_incumbent(&world)?; + + // The incumbent's own activation is accounted for. + project_admitted(&world, &incumbent)?; + assert_eq!( + world.engine.reconcile_failed_continuity_projections()?, + ProjectionReconciliation::default() + ); + + // Some other activation, issued by nothing in history, is reported and + // left exactly as it is. + let stranger = seeded_transaction( + &world, + DeploymentPhase::Warming, + CommandKind::Continuity, + Some(&incumbent), + )?; + let anchor = world.engine.provider_anchor_for_plan(&incumbent.plan)?; + let topology = world.engine.topology(); + topology.withdraw_stale_incarnation(&incumbent.expected, &anchor)?; + project_candidate(&world, &stranger, true)?; + // A live transaction accounts for its own activation... + assert_eq!( + world.engine.reconcile_failed_continuity_projections()?, + ProjectionReconciliation::default() + ); + // ...and once it is gone, nothing does. + let seeded = resident(&world)?; + let command = ControlSnapshot::read(&world.state_store)? + .commands + .into_iter() + .find(|stored| stored.value.command_id == seeded.value.command_id) + .context("seeded command")?; + assert!( + SingleFileMessagePackBackingStore::new(&world.state_store) + .delete_batch_if_unchanged(&[seeded.envelope.clone(), command.envelope])? + ); + let projection = std::fs::read(&world.engine.options.topology_store)?; + let outcome = world.engine.reconcile_failed_continuity_projections()?; + assert_eq!( + outcome, + ProjectionReconciliation { + demoted: Vec::new(), + unexplained: vec!["service".into()], + } + ); + assert_eq!(std::fs::read(&world.engine.options.topology_store)?, projection); + Ok(()) + } + + // ---- fix 4: history that cannot be read stops actuation ---- + + /// An operator binding for the target "service", so the command queue can + /// resolve selectors. + fn write_service_binding(world: &EngineFixture) -> Result<()> { + use crate::deployment_plan::tests::BINDING; + let (head, tail) = BINDING + .split_once("[route]") + .context("binding has no route table")?; + let binding = format!( + "{head}[brakes]{}", + tail.split_once("[brakes]") + .context("binding has no brakes table")? + .1 + ); + std::fs::create_dir_all(&world.engine.options.bindings_dir)?; + std::fs::write(world.engine.options.bindings_dir.join("service.toml"), binding)?; + Ok(()) + } + + fn corrupt_history(world: &EngineFixture) -> Result<()> { + std::fs::write(history_store_path(&world.state_store), b"\xc1\xc1 torn")?; + Ok(()) + } + + #[test] + fn history_that_cannot_be_read_is_an_error_and_missing_history_is_empty() -> Result<()> { + let world = EngineFixture::new()?; + let history = read_history(&world.state_store)?; + assert!(history.transactions.is_empty() && history.report().is_none()); + + corrupt_history(&world)?; + let error = read_history(&world.state_store).err().context("torn history read")?; + assert!(format!("{error:#}").contains("history"), "{error:#}"); + assert!(world.engine.history_for_decision().is_none()); + // The display path still answers, with nothing. + assert!(read_history_transactions(&world.state_store).is_empty()); + Ok(()) + } + + #[test] + fn undecodable_history_names_what_it_is_missing() -> Result<()> { + let (finished, _) = terminal_transaction_with_command("ghostlight")?; + let good = transaction_envelope(&finished, finished.updated_at_unix_millis)?; + let mut torn = good.clone(); + torn.key = "tx-torn".into(); + torn.payload = vec![0xc1]; + let (transactions, undecodable) = decode_history_transactions(vec![good, torn]); + let history = HistoryRead { + transactions, + undecodable, + }; + let report = history.report().context("nothing reported")?; + assert!(report.contains("1 archived transaction"), "{report}"); + assert!(report.contains("tx-torn"), "{report}"); + + let complete = HistoryRead { + transactions: history.transactions, + undecodable: Vec::new(), + }; + assert_eq!(complete.report(), None); + Ok(()) + } + + #[test] + fn a_fault_is_said_once_while_it_lasts() { + let once = ReportOnce::default(); + assert_eq!(once.offer(None), None); + assert_eq!(once.offer(Some("torn".into())), Some("torn".into())); + assert_eq!(once.offer(Some("torn".into())), None); + assert_eq!(once.offer(Some("torn".into())), None); + assert_eq!(once.offer(Some("worse".into())), Some("worse".into())); + assert_eq!(once.offer(None), None); + assert_eq!(once.offer(Some("worse".into())), Some("worse".into())); + } + + #[test] + fn unreadable_history_stops_continuity_instead_of_counting_no_failures() -> Result<()> { + let workload = SwitchWorkload::new(); + let world = EngineFixture::with_workload(workload.clone())?; + admit_incumbent(&world)?; + workload.kill(); + corrupt_history(&world)?; + let envelope = incumbent_envelope(&world)?; + + // A dead workload would be restarted at once, with the ceiling counting + // zero refusals. With history unreadable nothing is actuated. + assert!(!world.engine.supervise_one_admitted_generation()?); + assert!(ControlSnapshot::read(&world.state_store)?.transactions.is_empty()); + assert_eq!(incumbent_envelope(&world)?, envelope); + + // Readable again, the same tick restarts it. + std::fs::remove_file(history_store_path(&world.state_store))?; + assert!(world.engine.supervise_one_admitted_generation()?); + assert_eq!(ControlSnapshot::read(&world.state_store)?.transactions.len(), 1); + Ok(()) + } + + #[test] + fn unreadable_history_stops_command_retirement_and_freezing() -> Result<()> { + let world = EngineFixture::new()?; + let command = DeploymentCommand { + schema_version: DEPLOYMENT_COMMAND_SCHEMA.into(), + command_id: "up-nowhere".into(), + kind: CommandKind::Deploy, + selector: "nowhere".into(), + requested_by: "test".into(), + requested_at_unix_millis: 100, + }; + assert!( + SingleFileMessagePackBackingStore::new(&world.state_store).compare_exchange( + &[CultCacheExpectedEnvelope { + r#type: DeploymentCommand::TYPE.into(), + key: command.command_id.clone(), + current: None, + }], + &[command_envelope(&command, 100)?], + )? + ); + write_service_binding(&world)?; + corrupt_history(&world)?; + + // Whether the command was consumed is unknown, so it is not acted on. + assert!(!world.engine.freeze_one_queued_command()?); + assert!(ControlSnapshot::read(&world.state_store)?.transactions.is_empty()); + + // Readable, it is acted on: the unknown selector is refused. + std::fs::remove_file(history_store_path(&world.state_store))?; + assert!(world.engine.freeze_one_queued_command()?); + assert_eq!(ControlSnapshot::read(&world.state_store)?.transactions.len(), 1); + Ok(()) + } } diff --git a/src/drivers.rs b/src/drivers.rs index 226906f..d56e3d9 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -4488,6 +4488,22 @@ impl CultCacheTopologyDriver { ) } + /// The activation the projection currently names under this incarnation's + /// key, if any. Read-only: it never judges whether the activation is the + /// right one, so a caller can tell an exact issuer from a substitute + /// before it demotes anything. + pub fn projected_activation( + &self, + expected: &IdunnExpectedIncarnationRecord, + ) -> Result> { + let key = incarnation_key(expected)?; + let entries = self.snapshot()?; + projection_entry(&entries, IdunnRuntimeActivationRecord::TYPE, &key)? + .map(|envelope| IdunnRuntimeActivationRecord::decode_canonical(&envelope.payload)) + .transpose() + .map_err(Into::into) + } + /// Every incarnation currently projected for one target, by its Expected. pub fn projected_incarnations( &self, diff --git a/tests/fixtures/idunn-control-legacy/README.md b/tests/fixtures/idunn-control-legacy/README.md index 07086d7..d1b00ec 100644 --- a/tests/fixtures/idunn-control-legacy/README.md +++ b/tests/fixtures/idunn-control-legacy/README.md @@ -22,3 +22,23 @@ the plan is the `deployment_plan` test recipe. No byte comes from a live store. `control_plane.rs` decodes these through `LegacyDeploymentTransaction` and `LegacyAdmittedGeneration`. The fixtures die with those structs. + +## Pre-B1 continuity aborts + +The `transaction-*-abort*` files and `provider-anchor` were printed by a second +throwaway test over `9001b58`. It built a continuity transaction that issued an +activation (the recipe and binding of the `deployment_plan` tests, a provider +identity enrolled fresh, the trust-anchor path in the binding fixed at +`/tmp/idunn-b1-legacy/provider-anchor.cc`) and gave it the aborts the base code +writes: the pre-fence abort literal of `begin_pre_fencing_abort`, and the base +`post_fencing_abort_intent`. In each, the transaction issued an activation and +its abort records `topology_reconciliation = Skipped`, because the base rule +owed a continuity no projection cleanup. The workload and Odin evidence are +borrowed from `transaction-fencing`. + +| File | What it is | +|---|---| +| `transaction-pre-fence-abort` | Warming continuity, pre-fencing abort in flight | +| `transaction-pre-fence-abort-terminal` | the same abort finished: Complete, `FailedBeforeFencing` | +| `transaction-post-fence-abort` | Fencing continuity, post-fencing abort in flight | +| `provider-anchor` | one line: the provider trust-anchor file bytes, hex. A test writes it to the fixed path so the Engine can read the plan's anchor | diff --git a/tests/fixtures/idunn-control-legacy/provider-anchor.hex b/tests/fixtures/idunn-control-legacy/provider-anchor.hex new file mode 100644 index 0000000..928d4cf --- /dev/null +++ b/tests/fixtures/idunn-control-legacy/provider-anchor.hex @@ -0,0 +1 @@ +93b263756c7463616368652e73746f72652e76319197d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631d93467616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e76312e7631d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631d9967b22736368656d614e616d65223a2267616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631222c22736368656d6156657273696f6e223a2267616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e76312e7631222c226d656d62657273223a5b5d7d91d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631909194d92867616d6563756c742d70726f76696465722d6865616c74682d6964656e746974792d7075626c6963d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631d923323032362d30392d32395430333a34353a31352e3031353232313330302b30303a3030c5014596d93167616d6563756c742e70726f76696465725f6865616c74685f6964656e746974792e74727573745f616e63686f722e7631d94031626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337dc00205858cca9ccbb7dccb9cc8d01cccfccfccc9153ccdbcce23c224bcca757617dcce5cc8a163a6516cc9dccee2c2acce9d92d6f735f696e7374616c6c6174696f6e5f66696c655f626f756e645f636c6f6e6561626c655f626173656c696e65d923323032362d30392d32395430333a34353a31352e3031353232313330302b30303a3030d9477368613235362d63663933623766656534396235313133343736366538613937623432303065653636396338353164386464336634353366623537666664326233366630653430 diff --git a/tests/fixtures/idunn-control-legacy/transaction-post-fence-abort.hex b/tests/fixtures/idunn-control-legacy/transaction-post-fence-abort.hex new file mode 100644 index 0000000..37cd704 --- /dev/null +++ b/tests/fixtures/idunn-control-legacy/transaction-post-fence-abort.hex @@ -0,0 +1,2 @@ +idunn.deployment_transaction.v3 +dc0023bf6964756e6e2e6465706c6f796d656e745f7472616e73616374696f6e2e7633d92774782d39356138313166662d303533332d343562622d613031352d303038356132353835633061b2636f6e74696e756974792d73657276696365aa636f6e74696e75697479a77365727669636500a766656e63696e67cf0000018bcfe56800cf0000018bcfe56800c099d9216964756e6e2e636f6d70696c65645f6465706c6f796d656e745f706c616e2e7632d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d316e9bbf6964756e6e2e736f757263655f73656c656374696f6e5f66616374732e7631d92768747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974af726566732f68656164732f6d61696ed92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92833333333333333333333333333333333333333333333333333333333333333333333333333333333bc6465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6cd9477368613235362d323739336466633034633331313961373962353037363965613637356538633030613938366665623331393939306631623031636364363833343563363732618091a87265662d6865616464dc04e20a736368656d61203d202267616d6563756c742e6964756e6e2e7461726765745f6465636c61726174696f6e2e7631220a746172676574203d202273657276696365220a736f757263655f7374616d705f656e7669726f6e6d656e74203d2022534552564943455f4255494c445f434f4d4d4954220a0a5b5b73746570735d5d0a6964203d20226275696c64220a7068617365203d20226275696c64220a72756e6e6572203d202272757374220a61726776203d205b22636172676f222c20226275696c64222c20222d2d6c6f636b6564225d0a0a5b5b65787465726e616c5f696e707574735d5d0a6964203d2022746f6f6c636861696e2d696e646578220a75726c203d202268747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578220a736861323536203d202264646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464220a72756e6e6572203d202272757374220a64657374696e6174696f6e203d2022696e707574732f746f6f6c636861696e2d696e646578220a0a5b5b6172746966616374735d5d0a6964203d20226461656d6f6e220a736f757263655f6b696e64203d202272756e6e65722d6f7574707574220a72756e6e6572203d202272757374220a736f75726365203d20227461726765742f72656c656173652f73657276696365220a64657374696e6174696f6e203d202273657276696365220a65787065637465645f736861323536203d202263636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363220a65786563757461626c65203d20747275650a0a5b736572766963655d0a65786563757461626c655f6172746966616374203d20226461656d6f6e220a617267756d656e7473203d205b0a20207b206b696e64203d202262696e64696e67222c206e616d65203d202273746174655f726f6f7422207d2c0a5d0a7472616e73706f7274203d202268747470220a726f7574655f7265717569726564203d2066616c73650a72657175697265645f656e7669726f6e6d656e74203d205b2247414d4543554c545f4944554e4e5f52554e54494d455f42554e444c45225d0a0a5b736572766963652e6865616c74685d0a636f6e7472616374203d2022736572766963652e6865616c7468220a0a5b73746174655d0a736368656d615f67656e65726174696f6e203d20227631220a0a5b5b73746174652e736c6f74735d5d0a6964203d202272756e74696d652d6361636865220a72656c61746976655f70617468203d202272756e74696d652d63616368652e6363220a6b696e64203d202263756c7463616368652d66696c65220a736368656d61203d2022736572766963652e72756e74696d652d63616368652e7631220a777269746572203d20226e6f6e65220a7265636f76657279203d202272656275696c6461626c65220a73746172747570203d20226f70656e2d61742d7374617274220a0a5b5b70726f76696465735d5d0a6361706162696c697479203d2022736572766963652e72756e74696d65220a736368656d61203d2022736572766963652e72756e74696d652e7631220a636f6d7061746962696c697479203d20227631220a0adc06930a736368656d61203d202267616d6563756c742e6964756e6e2e6f70657261746f725f62696e64696e672e7632220a746172676574203d202273657276696365220a0a5b7265706f7369746f72795d0a6f726967696e203d202268747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974220a61646d69747465645f726566203d2022726566732f68656164732f6d61696e220a6d696e696d756d5f7265766973696f6e203d202231313131313131313131313131313131313131313131313131313131313131313131313131313131220a73656c656374696f6e203d20227265662d68656164220a636865636b6f7574203d20222f7372762f6275696c642f53657276696365220a7265636970655f70617468203d20226465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6c220a0a5b72756e6e6572732e727573745d0a647269766572203d2022646f636b6572220a696d616765203d202272757374407368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161220a75736572203d2022313030303a31303030220a6166666f7264616e636573203d205b22736f757263652d72656164222c202261727469666163742d7772697465225d0a616c6c6f7765645f70726f6772616d73203d205b22636172676f225d0a6e6574776f726b5f70726f66696c65203d20226275696c642d646570656e64656e63792d656772657373220a6d656d6f72795f6d6562696279746573203d20323034380a6370755f71756f74615f70657263656e74203d203230300a706964735f6c696d6974203d203531320a746d7066735f6d6562696279746573203d203531320a0a5b776f726b6c6f61645d0a647269766572203d202273797374656d642d7472616e7369656e74220a73746174655f67726f7570203d202273657276696365220a756e69745f707265666978203d20226964756e6e2d73657276696365220a72656c656173655f726f6f74203d20222f7372762f736572766963652f72656c6561736573220a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a72756e74696d655f726f6f74203d20222f72756e2f67616d6563756c742f6964756e6e2f73657276696365220a6e6574776f726b203d2022686f73742d70726976617465220a68617264656e696e67203d2022737472696374220a6d656d6f72795f6d6562696279746573203d20313032340a6370755f71756f74615f70657263656e74203d203130300a0a5b776f726b6c6f61642e617267756d656e745f62696e64696e67735d0a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a0a5b776f726b6c6f61642e7365637265745f66696c65735d0a47414d4543554c545f52554e54494d455f50524553454e43455f4944454e54495459203d20222f6574632f67616d6563756c742f736572766963652f72756e74696d652d70726573656e63652d6964656e746974792e6363220a0a5b72756e74696d655f6964656e746974795d0a72756e74696d655f6964203d2022736572766963652d79676764726173696c220a65787065637465645f7369676e65725f6964656e746974795f6964203d202231626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337220a74727573745f616e63686f725f73746f7265203d20222f746d702f6964756e6e2d62312d6c65676163792f70726f76696465722d616e63686f722e6363220a0a5b6272616b65735d0a6465706c6f796d656e745f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6465706c6f796d656e742d6272616b652e6363220a6c6966656379636c655f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6c6966656379636c652d6272616b652e6363220a0a5b726f6c6c6f75745d0a7374726174656779203d202263616e6469646174652d7468656e2d70726f6d6f7465220a647261696e5f7365636f6e6473203d2033300a72657461696e5f72656c6561736573203d20320a0a5b706c6163656d656e745d0a646573697265645f7265706c69636173203d20310a6e6f646573203d205b2279676764726173696c225d0a90c0c096b76964756e6e2e7365616c65645f72656c656173652e7631d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662d9477368613235362d386265323063646333353739303164633733653161626232623363656666333738353436613239623431613535356561316436336535303665623337623763309195a66461656d6f6ea773657276696365d9477368613235362d636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363632ac39196af746f6f6c636861696e2d696e646578d92768747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578d9477368613235362d64646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464a472757374b6696e707574732f746f6f6c636861696e2d696e646578117892d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662ba2f7372762f736572766963652f72656c65617365732f74657374dc0012bd6964756e6e2e65787065637465645f696e6361726e6174696f6e2e7632a773657276696365d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d31d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662bb6769746875622e636f6d2f47616d6543756c742f53657276696365d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d9477368613235362d32373933646663303463333131396137396235303736396561363735653863303061393836666562333139393930663162303163636436383334356336373261b1736572766963652d79676764726173696cd94031626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337ae736572766963652e6865616c7468d9477368613235362d63636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a27631d9477368613235362d32663837303134316366353638316566656332653234316364636435663731303831616539326162316535633534633839636438393666386335656236323731c2c09194af736572766963652e72756e74696d65b2736572766963652e72756e74696d652e7631a276310190d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666c0cf0000018bcfe568009abb6964756e6e2e72756e74696d655f61637469766174696f6e2e7632d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666b1736572766963652d79676764726173696cd9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137d94066313063623131353633346366613935383966356564616332313730343339363933353834343234643736653133366434396661613236373539323266376665c420f2977384801339144d13ebd2ab001bf29fbab79f7c3948334d440db8206cf974cf0000018bcfe56800d94063653461663030623963626662316362613963363136363965323337303232323465343763623163303761346233313431626634353237383236353965646637a765643235353139c4403a548c511ff3de6fcac116b903fa6badb14deef79dd81958d71947af575fae753b11692d0272c7cb5c437300cdd0d5315e28a1e7e12f9df0a0b817951bba4700dc0033b26964756e6e2d343234322e73657276696365af4964756e6e20746573742034323432af696e766f636174696f6e2d3432343201a465786563a6616c77617973ad636f6e74726f6c2d67726f7570c3a57534323432a57534323432a0a0a0c3c3a9696e76697369626c65a3706964c3a430303737a0a0cd10920194cd1092cd1092cd1092cd109294cd1092cd1092cd1092cd109291cd10920000000000c391010807b52f6f70742f746573742f62696e2f736572766963650101d9477368613235362d34626635313232663334343535346335336264653265626238636432623765336431363030616436333163333835613564376363653233633737383534353961d9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137a92f6f70742f74657374a92f72756e2f74657374d9477368613235362d6462633162346339303066666534386435373562356461356336333830343031323566363564623066653365323434393462373665613938363435376439383690d9477368613235362d30383466656430386239373861663464376431393661373434366138366235383030396536333662363131646231363231316236356139616164666632396335d9202f73797374656d2e736c6963652f6964756e6e2d343234322e73657276696365c090aa61637469766174696f6edc0020010101010101010101010101010101010101010101010101010101010101010190d9477368613235362d32393364343764383932393938373139346131303664303236326236326563363766633534393165366339633366653961326663383132643935633237376661c092ad6f64696e2d746f706f6c6f6779959139d9477368613235362d31393538316532376465376365643030666631636535306232303437653761353637633736623163626165626162653565663033663763333031376262356237ab6f64696e2d7369676e65722764c096cd10920708c0c0c091b1736b69707065642d73746174656c657373c0c0c029c0c0c0c096b163616e6469646174652072656675736564a7736b6970706564a7736b6970706564a770656e64696e67a7736b6970706564a7736b6970706564 diff --git a/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort-terminal.hex b/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort-terminal.hex new file mode 100644 index 0000000..29011d9 --- /dev/null +++ b/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort-terminal.hex @@ -0,0 +1,2 @@ +idunn.deployment_transaction.v3 +dc0023bf6964756e6e2e6465706c6f796d656e745f7472616e73616374696f6e2e7633d92774782d39356138313166662d303533332d343562622d613031352d303038356132353835633061b2636f6e74696e756974792d73657276696365aa636f6e74696e75697479a77365727669636500a8636f6d706c657465cf0000018bcfe56800cf0000018bcfe56800c099d9216964756e6e2e636f6d70696c65645f6465706c6f796d656e745f706c616e2e7632d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d316e9bbf6964756e6e2e736f757263655f73656c656374696f6e5f66616374732e7631d92768747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974af726566732f68656164732f6d61696ed92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92833333333333333333333333333333333333333333333333333333333333333333333333333333333bc6465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6cd9477368613235362d323739336466633034633331313961373962353037363965613637356538633030613938366665623331393939306631623031636364363833343563363732618091a87265662d6865616464dc04e20a736368656d61203d202267616d6563756c742e6964756e6e2e7461726765745f6465636c61726174696f6e2e7631220a746172676574203d202273657276696365220a736f757263655f7374616d705f656e7669726f6e6d656e74203d2022534552564943455f4255494c445f434f4d4d4954220a0a5b5b73746570735d5d0a6964203d20226275696c64220a7068617365203d20226275696c64220a72756e6e6572203d202272757374220a61726776203d205b22636172676f222c20226275696c64222c20222d2d6c6f636b6564225d0a0a5b5b65787465726e616c5f696e707574735d5d0a6964203d2022746f6f6c636861696e2d696e646578220a75726c203d202268747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578220a736861323536203d202264646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464220a72756e6e6572203d202272757374220a64657374696e6174696f6e203d2022696e707574732f746f6f6c636861696e2d696e646578220a0a5b5b6172746966616374735d5d0a6964203d20226461656d6f6e220a736f757263655f6b696e64203d202272756e6e65722d6f7574707574220a72756e6e6572203d202272757374220a736f75726365203d20227461726765742f72656c656173652f73657276696365220a64657374696e6174696f6e203d202273657276696365220a65787065637465645f736861323536203d202263636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363220a65786563757461626c65203d20747275650a0a5b736572766963655d0a65786563757461626c655f6172746966616374203d20226461656d6f6e220a617267756d656e7473203d205b0a20207b206b696e64203d202262696e64696e67222c206e616d65203d202273746174655f726f6f7422207d2c0a5d0a7472616e73706f7274203d202268747470220a726f7574655f7265717569726564203d2066616c73650a72657175697265645f656e7669726f6e6d656e74203d205b2247414d4543554c545f4944554e4e5f52554e54494d455f42554e444c45225d0a0a5b736572766963652e6865616c74685d0a636f6e7472616374203d2022736572766963652e6865616c7468220a0a5b73746174655d0a736368656d615f67656e65726174696f6e203d20227631220a0a5b5b73746174652e736c6f74735d5d0a6964203d202272756e74696d652d6361636865220a72656c61746976655f70617468203d202272756e74696d652d63616368652e6363220a6b696e64203d202263756c7463616368652d66696c65220a736368656d61203d2022736572766963652e72756e74696d652d63616368652e7631220a777269746572203d20226e6f6e65220a7265636f76657279203d202272656275696c6461626c65220a73746172747570203d20226f70656e2d61742d7374617274220a0a5b5b70726f76696465735d5d0a6361706162696c697479203d2022736572766963652e72756e74696d65220a736368656d61203d2022736572766963652e72756e74696d652e7631220a636f6d7061746962696c697479203d20227631220a0adc06930a736368656d61203d202267616d6563756c742e6964756e6e2e6f70657261746f725f62696e64696e672e7632220a746172676574203d202273657276696365220a0a5b7265706f7369746f72795d0a6f726967696e203d202268747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974220a61646d69747465645f726566203d2022726566732f68656164732f6d61696e220a6d696e696d756d5f7265766973696f6e203d202231313131313131313131313131313131313131313131313131313131313131313131313131313131220a73656c656374696f6e203d20227265662d68656164220a636865636b6f7574203d20222f7372762f6275696c642f53657276696365220a7265636970655f70617468203d20226465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6c220a0a5b72756e6e6572732e727573745d0a647269766572203d2022646f636b6572220a696d616765203d202272757374407368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161220a75736572203d2022313030303a31303030220a6166666f7264616e636573203d205b22736f757263652d72656164222c202261727469666163742d7772697465225d0a616c6c6f7765645f70726f6772616d73203d205b22636172676f225d0a6e6574776f726b5f70726f66696c65203d20226275696c642d646570656e64656e63792d656772657373220a6d656d6f72795f6d6562696279746573203d20323034380a6370755f71756f74615f70657263656e74203d203230300a706964735f6c696d6974203d203531320a746d7066735f6d6562696279746573203d203531320a0a5b776f726b6c6f61645d0a647269766572203d202273797374656d642d7472616e7369656e74220a73746174655f67726f7570203d202273657276696365220a756e69745f707265666978203d20226964756e6e2d73657276696365220a72656c656173655f726f6f74203d20222f7372762f736572766963652f72656c6561736573220a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a72756e74696d655f726f6f74203d20222f72756e2f67616d6563756c742f6964756e6e2f73657276696365220a6e6574776f726b203d2022686f73742d70726976617465220a68617264656e696e67203d2022737472696374220a6d656d6f72795f6d6562696279746573203d20313032340a6370755f71756f74615f70657263656e74203d203130300a0a5b776f726b6c6f61642e617267756d656e745f62696e64696e67735d0a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a0a5b776f726b6c6f61642e7365637265745f66696c65735d0a47414d4543554c545f52554e54494d455f50524553454e43455f4944454e54495459203d20222f6574632f67616d6563756c742f736572766963652f72756e74696d652d70726573656e63652d6964656e746974792e6363220a0a5b72756e74696d655f6964656e746974795d0a72756e74696d655f6964203d2022736572766963652d79676764726173696c220a65787065637465645f7369676e65725f6964656e746974795f6964203d202231626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337220a74727573745f616e63686f725f73746f7265203d20222f746d702f6964756e6e2d62312d6c65676163792f70726f76696465722d616e63686f722e6363220a0a5b6272616b65735d0a6465706c6f796d656e745f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6465706c6f796d656e742d6272616b652e6363220a6c6966656379636c655f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6c6966656379636c652d6272616b652e6363220a0a5b726f6c6c6f75745d0a7374726174656779203d202263616e6469646174652d7468656e2d70726f6d6f7465220a647261696e5f7365636f6e6473203d2033300a72657461696e5f72656c6561736573203d20320a0a5b706c6163656d656e745d0a646573697265645f7265706c69636173203d20310a6e6f646573203d205b2279676764726173696c225d0a90c0c096b76964756e6e2e7365616c65645f72656c656173652e7631d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662d9477368613235362d386265323063646333353739303164633733653161626232623363656666333738353436613239623431613535356561316436336535303665623337623763309195a66461656d6f6ea773657276696365d9477368613235362d636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363632ac39196af746f6f6c636861696e2d696e646578d92768747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578d9477368613235362d64646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464a472757374b6696e707574732f746f6f6c636861696e2d696e646578117892d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662ba2f7372762f736572766963652f72656c65617365732f74657374dc0012bd6964756e6e2e65787065637465645f696e6361726e6174696f6e2e7632a773657276696365d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d31d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662bb6769746875622e636f6d2f47616d6543756c742f53657276696365d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d9477368613235362d32373933646663303463333131396137396235303736396561363735653863303061393836666562333139393930663162303163636436383334356336373261b1736572766963652d79676764726173696cd94031626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337ae736572766963652e6865616c7468d9477368613235362d63636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a27631d9477368613235362d32663837303134316366353638316566656332653234316364636435663731303831616539326162316535633534633839636438393666386335656236323731c2c09194af736572766963652e72756e74696d65b2736572766963652e72756e74696d652e7631a276310190d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666c0cf0000018bcfe568009abb6964756e6e2e72756e74696d655f61637469766174696f6e2e7632d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666b1736572766963652d79676764726173696cd9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137d94066313063623131353633346366613935383966356564616332313730343339363933353834343234643736653133366434396661613236373539323266376665c420f2977384801339144d13ebd2ab001bf29fbab79f7c3948334d440db8206cf974cf0000018bcfe56800d94063653461663030623963626662316362613963363136363965323337303232323465343763623163303761346233313431626634353237383236353965646637a765643235353139c4403a548c511ff3de6fcac116b903fa6badb14deef79dd81958d71947af575fae753b11692d0272c7cb5c437300cdd0d5315e28a1e7e12f9df0a0b817951bba4700dc0033b26964756e6e2d343234322e73657276696365af4964756e6e20746573742034323432af696e766f636174696f6e2d3432343201a465786563a6616c77617973ad636f6e74726f6c2d67726f7570c3a57534323432a57534323432a0a0a0c3c3a9696e76697369626c65a3706964c3a430303737a0a0cd10920194cd1092cd1092cd1092cd109294cd1092cd1092cd1092cd109291cd10920000000000c391010807b52f6f70742f746573742f62696e2f736572766963650101d9477368613235362d34626635313232663334343535346335336264653265626238636432623765336431363030616436333163333835613564376363653233633737383534353961d9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137a92f6f70742f74657374a92f72756e2f74657374d9477368613235362d6462633162346339303066666534386435373562356461356336333830343031323566363564623066653365323434393462373665613938363435376439383690d9477368613235362d30383466656430386239373861663464376431393661373434366138366235383030396536333662363131646231363231316236356139616164666632396335d9202f73797374656d2e736c6963652f6964756e6e2d343234322e73657276696365c090aa61637469766174696f6edc0020010101010101010101010101010101010101010101010101010101010101010190d9477368613235362d32393364343764383932393938373139346131303664303236326236326563363766633534393165366339633366653961326663383132643935633237376661c092ad6f64696e2d746f706f6c6f6779959139d9477368613235362d31393538316532376465376365643030666631636535306232303437653761353637633736623163626165626162653565663033663763333031376262356237ab6f64696e2d7369676e65722764c0c0c0c0c0c029b163616e646964617465207265667573656492b56661696c65642d6265666f72652d66656e63696e67b163616e646964617465207265667573656494b163616e6469646174652072656675736564a8636f6d706c657465a7736b6970706564a7736b6970706564c0c0 diff --git a/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort.hex b/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort.hex new file mode 100644 index 0000000..ae345b1 --- /dev/null +++ b/tests/fixtures/idunn-control-legacy/transaction-pre-fence-abort.hex @@ -0,0 +1,2 @@ +idunn.deployment_transaction.v3 +dc0023bf6964756e6e2e6465706c6f796d656e745f7472616e73616374696f6e2e7633d92774782d39356138313166662d303533332d343562622d613031352d303038356132353835633061b2636f6e74696e756974792d73657276696365aa636f6e74696e75697479a77365727669636500a77761726d696e67cf0000018bcfe56800cf0000018bcfe56800c099d9216964756e6e2e636f6d70696c65645f6465706c6f796d656e745f706c616e2e7632d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d316e9bbf6964756e6e2e736f757263655f73656c656374696f6e5f66616374732e7631d92768747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974af726566732f68656164732f6d61696ed92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d92833333333333333333333333333333333333333333333333333333333333333333333333333333333bc6465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6cd9477368613235362d323739336466633034633331313961373962353037363965613637356538633030613938366665623331393939306631623031636364363833343563363732618091a87265662d6865616464dc04e20a736368656d61203d202267616d6563756c742e6964756e6e2e7461726765745f6465636c61726174696f6e2e7631220a746172676574203d202273657276696365220a736f757263655f7374616d705f656e7669726f6e6d656e74203d2022534552564943455f4255494c445f434f4d4d4954220a0a5b5b73746570735d5d0a6964203d20226275696c64220a7068617365203d20226275696c64220a72756e6e6572203d202272757374220a61726776203d205b22636172676f222c20226275696c64222c20222d2d6c6f636b6564225d0a0a5b5b65787465726e616c5f696e707574735d5d0a6964203d2022746f6f6c636861696e2d696e646578220a75726c203d202268747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578220a736861323536203d202264646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464220a72756e6e6572203d202272757374220a64657374696e6174696f6e203d2022696e707574732f746f6f6c636861696e2d696e646578220a0a5b5b6172746966616374735d5d0a6964203d20226461656d6f6e220a736f757263655f6b696e64203d202272756e6e65722d6f7574707574220a72756e6e6572203d202272757374220a736f75726365203d20227461726765742f72656c656173652f73657276696365220a64657374696e6174696f6e203d202273657276696365220a65787065637465645f736861323536203d202263636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363220a65786563757461626c65203d20747275650a0a5b736572766963655d0a65786563757461626c655f6172746966616374203d20226461656d6f6e220a617267756d656e7473203d205b0a20207b206b696e64203d202262696e64696e67222c206e616d65203d202273746174655f726f6f7422207d2c0a5d0a7472616e73706f7274203d202268747470220a726f7574655f7265717569726564203d2066616c73650a72657175697265645f656e7669726f6e6d656e74203d205b2247414d4543554c545f4944554e4e5f52554e54494d455f42554e444c45225d0a0a5b736572766963652e6865616c74685d0a636f6e7472616374203d2022736572766963652e6865616c7468220a0a5b73746174655d0a736368656d615f67656e65726174696f6e203d20227631220a0a5b5b73746174652e736c6f74735d5d0a6964203d202272756e74696d652d6361636865220a72656c61746976655f70617468203d202272756e74696d652d63616368652e6363220a6b696e64203d202263756c7463616368652d66696c65220a736368656d61203d2022736572766963652e72756e74696d652d63616368652e7631220a777269746572203d20226e6f6e65220a7265636f76657279203d202272656275696c6461626c65220a73746172747570203d20226f70656e2d61742d7374617274220a0a5b5b70726f76696465735d5d0a6361706162696c697479203d2022736572766963652e72756e74696d65220a736368656d61203d2022736572766963652e72756e74696d652e7631220a636f6d7061746962696c697479203d20227631220a0adc06930a736368656d61203d202267616d6563756c742e6964756e6e2e6f70657261746f725f62696e64696e672e7632220a746172676574203d202273657276696365220a0a5b7265706f7369746f72795d0a6f726967696e203d202268747470733a2f2f6769746875622e636f6d2f47616d6543756c742f536572766963652e676974220a61646d69747465645f726566203d2022726566732f68656164732f6d61696e220a6d696e696d756d5f7265766973696f6e203d202231313131313131313131313131313131313131313131313131313131313131313131313131313131220a73656c656374696f6e203d20227265662d68656164220a636865636b6f7574203d20222f7372762f6275696c642f53657276696365220a7265636970655f70617468203d20226465706c6f796d656e742f6964756e6e2f7265636970652e746f6d6c220a0a5b72756e6e6572732e727573745d0a647269766572203d2022646f636b6572220a696d616765203d202272757374407368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161220a75736572203d2022313030303a31303030220a6166666f7264616e636573203d205b22736f757263652d72656164222c202261727469666163742d7772697465225d0a616c6c6f7765645f70726f6772616d73203d205b22636172676f225d0a6e6574776f726b5f70726f66696c65203d20226275696c642d646570656e64656e63792d656772657373220a6d656d6f72795f6d6562696279746573203d20323034380a6370755f71756f74615f70657263656e74203d203230300a706964735f6c696d6974203d203531320a746d7066735f6d6562696279746573203d203531320a0a5b776f726b6c6f61645d0a647269766572203d202273797374656d642d7472616e7369656e74220a73746174655f67726f7570203d202273657276696365220a756e69745f707265666978203d20226964756e6e2d73657276696365220a72656c656173655f726f6f74203d20222f7372762f736572766963652f72656c6561736573220a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a72756e74696d655f726f6f74203d20222f72756e2f67616d6563756c742f6964756e6e2f73657276696365220a6e6574776f726b203d2022686f73742d70726976617465220a68617264656e696e67203d2022737472696374220a6d656d6f72795f6d6562696279746573203d20313032340a6370755f71756f74615f70657263656e74203d203130300a0a5b776f726b6c6f61642e617267756d656e745f62696e64696e67735d0a73746174655f726f6f74203d20222f7661722f6c69622f67616d6563756c742f73657276696365220a0a5b776f726b6c6f61642e7365637265745f66696c65735d0a47414d4543554c545f52554e54494d455f50524553454e43455f4944454e54495459203d20222f6574632f67616d6563756c742f736572766963652f72756e74696d652d70726573656e63652d6964656e746974792e6363220a0a5b72756e74696d655f6964656e746974795d0a72756e74696d655f6964203d2022736572766963652d79676764726173696c220a65787065637465645f7369676e65725f6964656e746974795f6964203d202231626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337220a74727573745f616e63686f725f73746f7265203d20222f746d702f6964756e6e2d62312d6c65676163792f70726f76696465722d616e63686f722e6363220a0a5b6272616b65735d0a6465706c6f796d656e745f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6465706c6f796d656e742d6272616b652e6363220a6c6966656379636c655f73746f7265203d20222f7661722f6c69622f67616d6563756c742f6964756e6e2f736572766963652d6c6966656379636c652d6272616b652e6363220a0a5b726f6c6c6f75745d0a7374726174656779203d202263616e6469646174652d7468656e2d70726f6d6f7465220a647261696e5f7365636f6e6473203d2033300a72657461696e5f72656c6561736573203d20320a0a5b706c6163656d656e745d0a646573697265645f7265706c69636173203d20310a6e6f646573203d205b2279676764726173696c225d0a90c0c096b76964756e6e2e7365616c65645f72656c656173652e7631d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662d9477368613235362d386265323063646333353739303164633733653161626232623363656666333738353436613239623431613535356561316436336535303665623337623763309195a66461656d6f6ea773657276696365d9477368613235362d636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363632ac39196af746f6f6c636861696e2d696e646578d92768747470733a2f2f6578616d706c652e696e76616c69642f746f6f6c636861696e2d696e646578d9477368613235362d64646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464a472757374b6696e707574732f746f6f6c636861696e2d696e646578117892d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662ba2f7372762f736572766963652f72656c65617365732f74657374dc0012bd6964756e6e2e65787065637465645f696e6361726e6174696f6e2e7632a773657276696365d9477368613235362d38626532306364633335373930316463373365316162623262336365666633373835343661323962343161353535656131643633653530366562333762376330b5736572766963652d696e6361726e6174696f6e2d31d9477368613235362d39366633373231386438333663646364303037393631343233333032353334333362316666633766323538633135353037656536376163323961643665663662bb6769746875622e636f6d2f47616d6543756c742f53657276696365d92832323232323232323232323232323232323232323232323232323232323232323232323232323232d9477368613235362d32373933646663303463333131396137396235303736396561363735653863303061393836666562333139393930663162303163636436383334356336373261b1736572766963652d79676764726173696cd94031626661653932326138626630313132373562353536623236393135303363386366643538313234383931373134323365373736663133386463313235376337ae736572766963652e6865616c7468d9477368613235362d63636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a27631d9477368613235362d32663837303134316366353638316566656332653234316364636435663731303831616539326162316535633534633839636438393666386335656236323731c2c09194af736572766963652e72756e74696d65b2736572766963652e72756e74696d652e7631a276310190d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666c0cf0000018bcfe568009abb6964756e6e2e72756e74696d655f61637469766174696f6e2e7632d9477368613235362d33336163613963353639636165373831643936396632313864633036613035313134613863666239333436313634633033613263336435343033613636323666b1736572766963652d79676764726173696cd9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137d94066313063623131353633346366613935383966356564616332313730343339363933353834343234643736653133366434396661613236373539323266376665c420f2977384801339144d13ebd2ab001bf29fbab79f7c3948334d440db8206cf974cf0000018bcfe56800d94063653461663030623963626662316362613963363136363965323337303232323465343763623163303761346233313431626634353237383236353965646637a765643235353139c4403a548c511ff3de6fcac116b903fa6badb14deef79dd81958d71947af575fae753b11692d0272c7cb5c437300cdd0d5315e28a1e7e12f9df0a0b817951bba4700dc0033b26964756e6e2d343234322e73657276696365af4964756e6e20746573742034323432af696e766f636174696f6e2d3432343201a465786563a6616c77617973ad636f6e74726f6c2d67726f7570c3a57534323432a57534323432a0a0a0c3c3a9696e76697369626c65a3706964c3a430303737a0a0cd10920194cd1092cd1092cd1092cd109294cd1092cd1092cd1092cd109291cd10920000000000c391010807b52f6f70742f746573742f62696e2f736572766963650101d9477368613235362d34626635313232663334343535346335336264653265626238636432623765336431363030616436333163333835613564376363653233633737383534353961d9477368613235362d66353962643235306336333830383661643330396235333539373636663232326266353033366333336230616633316239633935333130353831366636396137a92f6f70742f74657374a92f72756e2f74657374d9477368613235362d6462633162346339303066666534386435373562356461356336333830343031323566363564623066653365323434393462373665613938363435376439383690d9477368613235362d30383466656430386239373861663464376431393661373434366138366235383030396536333662363131646231363231316236356139616164666632396335d9202f73797374656d2e736c6963652f6964756e6e2d343234322e73657276696365c090aa61637469766174696f6edc0020010101010101010101010101010101010101010101010101010101010101010190d9477368613235362d32393364343764383932393938373139346131303664303236326236326563363766633534393165366339633366653961326663383132643935633237376661c092ad6f64696e2d746f706f6c6f6779959139d9477368613235362d31393538316532376465376365643030666631636535306232303437653761353637633736623163626165626162653565663033663763333031376262356237ab6f64696e2d7369676e65722764c0c0c0c0c0c029c0c094b163616e6469646174652072656675736564a770656e64696e67a7736b6970706564a7736b6970706564c0c0