From f127f4adb96ac4bd981b938d8a94fa7723a0d4ea Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Tue, 18 Aug 2026 23:18:58 +0100 Subject: [PATCH 1/6] chore(release): cut rc-2026.8.3 --- Cargo.lock | 28 +++++++++++++--------------- Cargo.toml | 6 +++--- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f96ed94..edc6257b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,7 +809,7 @@ dependencies = [ [[package]] name = "ant-node" -version = "0.17.1" +version = "0.17.2-rc.1" dependencies = [ "alloy", "ant-protocol", @@ -862,9 +862,8 @@ dependencies = [ [[package]] name = "ant-protocol" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f2b55e89a468584cfc91dc2f596583e089c17107de6f2f386f8339b665d1f0" +version = "2.3.3-rc.1" +source = "git+https://github.com/WithAutonomi/ant-protocol?branch=rc-2026.8.3#d7e994ead6a8be716436313c4b3655a256433045" dependencies = [ "blake3", "bytes", @@ -3106,7 +3105,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -4293,7 +4292,7 @@ dependencies = [ "quinn-udp 0.5.15", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -4332,7 +4331,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", "windows-sys 0.61.2", ] @@ -4345,7 +4344,7 @@ checksum = "76150b617afc75e6e21ac5f39bc196e80b65415ae48d62dbef8e2519d040ce42" dependencies = [ "cfg_aliases", "libc", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", "windows-sys 0.61.2", ] @@ -4920,9 +4919,8 @@ dependencies = [ [[package]] name = "saorsa-core" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3858333d71ea51750129693fdffc2962ad9ef95094c2ad76713dc7d5bb949c41" +version = "0.27.1-rc.1" +source = "git+https://github.com/WithAutonomi/saorsa-core?branch=rc-2026.8.3#b6fb91d0ca2300c1458ce384a21fac199cb3ecb5" dependencies = [ "anyhow", "async-trait", @@ -4932,6 +4930,7 @@ dependencies = [ "dirs 6.0.0", "futures", "hex", + "libc", "lru", "once_cell", "parking_lot", @@ -5035,9 +5034,8 @@ dependencies = [ [[package]] name = "saorsa-transport" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dbc21ea8af08c30a0d7d66289b987f14eb22e9ff1ed32e12c4e0a07c6d49d8" +version = "0.36.1-rc.1" +source = "git+https://github.com/WithAutonomi/saorsa-transport?branch=rc-2026.8.3#0fc30febc4fc00bb9e36cf9fc228d79429b5dfc9" dependencies = [ "anyhow", "async-trait", @@ -6418,7 +6416,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2b99f0f1..3e7f31e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ant-node" -version = "0.17.1" +version = "0.17.2-rc.1" edition = "2021" authors = ["David Irvine "] description = "Pure quantum-proof network node for the Autonomi decentralized network" @@ -39,10 +39,10 @@ mimalloc = "0.1" # Until then, the git pin tracks the matching saorsa-core lineage # (the rc-2026.4.2 branch) so Cargo can unify the wire types here # with ant-protocol's re-exports. -ant-protocol = "2.3.2" +ant-protocol = { git = "https://github.com/WithAutonomi/ant-protocol", branch = "rc-2026.8.3" } # Core (provides EVERYTHING: networking, DHT, security, trust, storage) -saorsa-core = "0.27.0" +saorsa-core = { git = "https://github.com/WithAutonomi/saorsa-core", branch = "rc-2026.8.3" } saorsa-pqc = "0.5" # Payment verification - autonomi network lookup + EVM payment From 365193b111d00feb25aa4c1e09aa6fd131968553 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 19 Aug 2026 21:38:48 +0100 Subject: [PATCH 2/6] chore(release): ship rc-2026.8.3 to beta as 0.17.2-beta.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edc6257b..2c02513f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,7 +809,7 @@ dependencies = [ [[package]] name = "ant-node" -version = "0.17.2-rc.1" +version = "0.17.2-beta.1" dependencies = [ "alloy", "ant-protocol", diff --git a/Cargo.toml b/Cargo.toml index 3e7f31e5..c432d090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ant-node" -version = "0.17.2-rc.1" +version = "0.17.2-beta.1" edition = "2021" authors = ["David Irvine "] description = "Pure quantum-proof network node for the Autonomi decentralized network" From 01f71a57fd5a0376ec5c3717e183572e0099189b Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 20 Aug 2026 16:28:04 +0100 Subject: [PATCH 3/6] fix: stop beta nodes restarting onto their own promotion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semver ranks a final above its own pre-release, so a node running 0.18.0-beta.1 treated the promoted 0.18.0 as an upgrade and applied it. That is a binary swap and a network restart for the same code re-tagged, and because the train promotes every cycle it happened on every train, to every beta node. It also quietly dissolved the cohort: a node enrolled in the beta programme spent part of each cycle running a stable build. On the beta channel a final is no longer a candidate when the running version is a beta pre-release of the same major.minor.patch. The skip is deliberately narrow — a genuinely newer final is still taken so a node does not stagnate if the beta line stalls, a later beta of the same version is still taken, and a node running a beta while configured for stable still takes the final, since that is its route back to the stable line. This depends on the pair (candidate, running version) rather than the candidate alone, so it sits beside the channel predicate rather than inside it. The beta-identifier test is now a shared helper so the two callers cannot drift. Accepted limitation, recorded in the ADR: if code changes between the last beta.N and the final without a new beta tag, a node skipping the promotion misses it until the next version. That is a process obligation on the train, not something the node can detect. Tests: 30 passing in upgrade::monitor, five new covering the skip, the newer-final case, the next-beta preference, beta.2, and stable being unaffected. Clippy, rustdoc and fmt clean. V2-1044 Co-Authored-By: Claude Opus 5 (1M context) --- ...ADR-0010-beta-upgrade-channel-semantics.md | 33 ++++- src/upgrade/monitor.rs | 115 +++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md b/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md index ffb98105..6903740b 100644 --- a/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md +++ b/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md @@ -70,6 +70,31 @@ the public `UpgradeMonitor::version_matches_channel`. Option 3 is deliberately deferred rather than rejected: nothing here prevents adding an `rc` channel later, and the exact-identifier rule means doing so is an additive change. +### Amendment: a beta node skips its own promotion + +Channel eligibility alone produced an unwanted hop. Semver ranks a final above its own +pre-release, so a node running `0.18.0-beta.1` treated the promoted `0.18.0` as an upgrade — a +binary swap and a network restart for what is the same code re-tagged. Because the train promotes +on every cycle, this would have happened on every train, to every beta node. + +Eligibility is therefore refined: **on the beta channel, a final release is not a candidate when the +running version is a `beta.*` pre-release of the same `major.minor.patch`.** The skip is narrow by +design: + +- A genuinely newer final is still taken (`0.19.0` while running `0.18.0-beta.1`), so a beta node + does not stagnate if the beta line stalls. +- A later beta of the same version is still taken (`0.18.0-beta.2`). +- It applies to the beta channel only. A node running a beta build while configured for `stable` + still takes the final, since that is its route back onto the stable line. + +This is a rule about the *pair* (candidate, running version) rather than about the candidate alone, +so it lives beside the channel predicate rather than inside it. + +Note the accepted limitation: if the train can change code between the last `beta.N` and the final +without cutting `beta.N+1`, a beta node skipping the promotion misses that change until the next +version. Guarding against that is a process obligation on the train — anything that changes after a +beta must get a new beta tag — not something the node can detect. + ## Consequences ### Positive @@ -79,6 +104,8 @@ later, and the exact-identifier rule means doing so is an additive change. - A beta node holds its soak build until a higher `-beta.N` or a final release appears. - The rule exists in one place, so the selection path and the predicate cannot diverge. - The `stable` channel is unchanged, so the production fleet is unaffected. +- Beta nodes restart once per train rather than twice, and keep their identity as beta builds + instead of being silently converted to stable ones on every promotion. ### Negative / Trade-offs @@ -87,6 +114,8 @@ later, and the exact-identifier rule means doing so is an additive change. capability reduction and is the main thing a reviewer should weigh. - Any future pre-release suffix is rejected by default. That is the safe direction, but it means a new suffix requires a deliberate code change rather than working implicitly. +- A beta node skipping its own promotion depends on the train tagging a new beta whenever code + changes after `beta.N`. If that discipline slips, the node holds an older build than it should. ### Neutral / Operational @@ -102,7 +131,9 @@ later, and the exact-identifier rule means doing so is an additive change. accepting `-beta.N` and finals while rejecting `-rc.N`, `-alpha.N` and `-betax.N`; selection over a mixed list (`0.16.0`, `0.17.0-beta.1`, `0.17.0-rc.1`) resolving to `0.16.0` on stable and `0.17.0-beta.1` on beta; and the ship-and-promote-same-day case hopping from `0.16.0-beta.1` - straight to `0.17.0-beta.1`. + straight to `0.17.0-beta.1`. For the amendment: a beta node ignoring its own promotion, still + taking a newer final, still taking a later beta of the same version, preferring the next beta over + its own promotion, and a stable-channel node still taking the promotion of a beta it is running. - End-to-end validation is tracked separately as V2-1012: a dev testnet where the cohort pulls a fake `-beta.N` release and demonstrably ignores a real rc published in the same window. - Review trigger: revisit this ADR if a new pre-release suffix is introduced, if internal rc soaking diff --git a/src/upgrade/monitor.rs b/src/upgrade/monitor.rs index cff66734..b24962dc 100644 --- a/src/upgrade/monitor.rs +++ b/src/upgrade/monitor.rs @@ -419,6 +419,11 @@ impl UpgradeMonitor { return None; } + if is_redundant_beta_promotion(&latest_version, &self.current_version, self.channel) { + debug!("Skipping {latest_version}: promotion of the beta already running"); + return None; + } + // Find platform assets let binary_asset = find_platform_asset(&release.assets)?; @@ -458,13 +463,48 @@ fn version_matches_channel(version: &Version, channel: UpgradeChannel) -> bool { match channel { UpgradeChannel::Stable => false, - UpgradeChannel::Beta => version.pre.as_str().split('.').next() == Some("beta"), + UpgradeChannel::Beta => is_beta_prerelease(version), } } +/// Whether a version's pre-release component marks it as a beta build. +/// +/// Matches on the exact first identifier, so `0.17.0-beta.1` and `0.17.0-beta` qualify while +/// `0.17.0-betax.1` does not. +#[must_use] +fn is_beta_prerelease(version: &Version) -> bool { + version.pre.as_str().split('.').next() == Some("beta") +} + +/// Whether upgrading to `candidate` would only trade a beta build for its own promotion. +/// +/// Promoting `X.Y.Z-beta.N` to the final `X.Y.Z` re-tags the same code, so a node already running +/// that beta would swap its binary and restart for no behavioural change. Semver ranks the final +/// above the pre-release, so without this the hop would happen on every release train, to every +/// beta node. +/// +/// Only the matching final is skipped. A genuinely newer final — `0.19.0` while running +/// `0.18.0-beta.1` — is still taken, so a node does not stagnate if the beta line stalls. +/// +/// Beta channel only: a node running a beta build while configured for `stable` should land on the +/// final, since that is its route back to the stable line. +#[must_use] +fn is_redundant_beta_promotion( + candidate: &Version, + current: &Version, + channel: UpgradeChannel, +) -> bool { + channel == UpgradeChannel::Beta + && candidate.pre.is_empty() + && is_beta_prerelease(current) + && (candidate.major, candidate.minor, candidate.patch) + == (current.major, current.minor, current.patch) +} + /// Select the most appropriate upgrade from a list of releases. /// -/// Only versions eligible for the channel are considered; see [`version_matches_channel`]. +/// Only versions eligible for the channel are considered, and a beta node skips the promotion of +/// the very build it is running; see `version_matches_channel` and `is_redundant_beta_promotion`. /// /// Returns the newest version that matches the channel and has platform assets. fn select_upgrade_from_releases( @@ -487,6 +527,11 @@ fn select_upgrade_from_releases( continue; } + if is_redundant_beta_promotion(&version, current_version, channel) { + debug!("Skipping {version}: promotion of the beta already running"); + continue; + } + let Some(binary_asset) = find_platform_asset(&release.assets) else { continue; }; @@ -1169,6 +1214,72 @@ mod tests { assert_eq!(beta.version, Version::parse("0.17.0-beta.1").unwrap()); } + /// A beta node does not restart onto the promotion of the build it is already running: + /// `0.18.0-beta.1` -> `0.18.0` re-tags the same code. + #[test] + fn test_select_upgrade_beta_skips_own_promotion() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![release_with_assets("v0.18.0")]; + + assert!( + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).is_none(), + "beta node should stay on 0.18.0-beta.1 when only its own promotion is published" + ); + } + + /// The skip is narrow: a genuinely newer final is still taken, so a beta node does not + /// stagnate if the beta line stalls. + #[test] + fn test_select_upgrade_beta_takes_newer_final() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![ + release_with_assets("v0.18.0"), + release_with_assets("v0.19.0"), + ]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + assert_eq!(upgrade.version, Version::parse("0.19.0").unwrap()); + } + + /// With its own promotion and a newer beta both published, the beta wins and the promotion is + /// skipped rather than taken first. + #[test] + fn test_select_upgrade_beta_prefers_next_beta_over_own_promotion() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![ + release_with_assets("v0.18.0"), + release_with_assets("v0.19.0-beta.1"), + ]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + assert_eq!(upgrade.version, Version::parse("0.19.0-beta.1").unwrap()); + } + + /// The skip is beta-channel only. A node running a beta build but configured for stable takes + /// the final, since that is its route back onto the stable line. + #[test] + fn test_select_upgrade_stable_takes_promotion_of_running_beta() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![release_with_assets("v0.18.0")]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable).unwrap(); + assert_eq!(upgrade.version, Version::parse("0.18.0").unwrap()); + } + + /// A later beta of the same version is still an upgrade — the skip only covers finals. + #[test] + fn test_select_upgrade_beta_takes_later_beta_of_same_version() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![release_with_assets("v0.18.0-beta.2")]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + assert_eq!(upgrade.version, Version::parse("0.18.0-beta.2").unwrap()); + } + /// Ship-and-promote on the same day: a node soaking `0.16.0-beta.1` sees both the promoted /// `0.16.0` and the next cut `0.17.0-beta.1`, and hops straight to the new beta. #[test] From 8cd695b31405b118ca3ed78b8c9f4fd919e30c73 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 20 Aug 2026 20:42:41 +0100 Subject: [PATCH 4/6] fix: compare the promotion skip against the committed beta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the previous commit found that the skip fired only for the beta a node was already running, not one it had selected but not yet applied. A node part-way through its staged rollout for 0.19.0-beta.1 is still running 0.18.0-beta.1, whose core differs from 0.19.0, so the final was not marked redundant and outranked the pending beta. The node applied the final and never ran the beta at all — meaning whether a node kept its beta identity depended on where its rollout jitter fell. Selection now compares against the build the node is committed to: the pending staged-rollout target when there is one, otherwise the running version. The "is it newer" guard deliberately stays on the running version, so a withdrawn pending release cannot leave a node refusing everything still published. ADR-0010 is corrected on a related point. It claimed promotion re-tags the same code; it does not. Dependency references flip from git branch pins to published crates.io versions, so a final is not byte-identical to its beta. The skip is safe for a different reason: any change landing on the RC branch produces a new beta, and the final is promoted from the most recent one, so no final carries code that no beta carries. The ADR now records that, along with its dependence on old beta releases staying published, and lists both as properties the node cannot verify. Tests: 32 passing in upgrade::monitor. Two added — a node holding its pending beta against that beta's promotion, and the same release pair without a pending target still resolving to the final, which documents why threading it through is necessary. Clippy, rustdoc and fmt clean. V2-1044 Co-Authored-By: Claude Opus 5 (1M context) --- ...ADR-0010-beta-upgrade-channel-semantics.md | 35 +++++++-- src/upgrade/monitor.rs | 78 ++++++++++++++++--- 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md b/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md index 6903740b..7712ae9c 100644 --- a/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md +++ b/docs/adr/ADR-0010-beta-upgrade-channel-semantics.md @@ -90,10 +90,28 @@ design: This is a rule about the *pair* (candidate, running version) rather than about the candidate alone, so it lives beside the channel predicate rather than inside it. -Note the accepted limitation: if the train can change code between the last `beta.N` and the final -without cutting `beta.N+1`, a beta node skipping the promotion misses that change until the next -version. Guarding against that is a process obligation on the train — anything that changes after a -beta must get a new beta tag — not something the node can detect. +The skip is safe because of how the train is shaped, not because the node can verify it. Promotion +does change the build: dependency references flip from git branch pins to published crates.io +versions (`ant-protocol = { git = ..., branch = "rc-2026.8.3" }` becomes `ant-protocol = "2.3.2"`), +so a final is not byte-identical to the beta it came from. What makes it the *same code* is that any +change landing on the RC branch produces a **new** beta release, and the final is promoted from the +most recent beta. There is therefore no window in which the final carries code that no beta carries. + +Two consequences follow, and both matter: + +- A node holding `X.Y.Z-beta.N` when `X.Y.Z-beta.N+1` and the final `X.Y.Z` are all published does + not strand. The later beta is eligible and outranks the one it is running, while the final is + skipped, so it converges on `beta.N+1` — the code that became the final. This depends on old beta + releases remaining published after promotion, which is the current policy. +- The comparison must be against the build the node is **committed to**, not the one it is running. + During a staged rollout those differ: a node that has selected `0.19.0-beta.1` but not yet applied + it is still running `0.18.0-beta.1`, whose core differs from `0.19.0`. Comparing against the + running version would let the final through and retarget the node onto it, so whether a node kept + its beta identity would depend on where its rollout jitter happened to fall. Selection therefore + takes the pending staged-rollout target when there is one. + +The "is it newer" guard deliberately stays on the running version, so that a withdrawn pending +release cannot leave a node refusing everything still published. ## Consequences @@ -114,8 +132,10 @@ beta must get a new beta tag — not something the node can detect. capability reduction and is the main thing a reviewer should weigh. - Any future pre-release suffix is rejected by default. That is the safe direction, but it means a new suffix requires a deliberate code change rather than working implicitly. -- A beta node skipping its own promotion depends on the train tagging a new beta whenever code - changes after `beta.N`. If that discipline slips, the node holds an older build than it should. +- The skip depends on two properties of the release process that the node cannot verify: that any + change after `beta.N` produces a `beta.N+1` which is what gets promoted, and that old beta + releases stay published so a node on a stale beta can converge through them. Neither is + mechanically enforced today; if either slips, a beta node holds an older build than it should. ### Neutral / Operational @@ -133,7 +153,8 @@ beta must get a new beta tag — not something the node can detect. `0.17.0-beta.1` on beta; and the ship-and-promote-same-day case hopping from `0.16.0-beta.1` straight to `0.17.0-beta.1`. For the amendment: a beta node ignoring its own promotion, still taking a newer final, still taking a later beta of the same version, preferring the next beta over - its own promotion, and a stable-channel node still taking the promotion of a beta it is running. + its own promotion, a stable-channel node still taking the promotion of a beta it is running, and a + node holding its pending beta when that beta's final is published mid-rollout. - End-to-end validation is tracked separately as V2-1012: a dev testnet where the cohort pulls a fake `-beta.N` release and demonstrably ignores a real rc published in the same window. - Review trigger: revisit this ADR if a new pre-release suffix is introduced, if internal rc soaking diff --git a/src/upgrade/monitor.rs b/src/upgrade/monitor.rs index b24962dc..b6d6b0a3 100644 --- a/src/upgrade/monitor.rs +++ b/src/upgrade/monitor.rs @@ -206,6 +206,7 @@ impl UpgradeMonitor { &cached_releases, &self.current_version, self.channel, + self.pending_upgrade_version.as_ref(), )); } @@ -227,6 +228,7 @@ impl UpgradeMonitor { &cached_releases, &self.current_version, self.channel, + self.pending_upgrade_version.as_ref(), )); } @@ -244,6 +246,7 @@ impl UpgradeMonitor { &releases, &self.current_version, self.channel, + self.pending_upgrade_version.as_ref(), )); } @@ -253,6 +256,7 @@ impl UpgradeMonitor { &releases, &self.current_version, self.channel, + self.pending_upgrade_version.as_ref(), )) } @@ -486,6 +490,12 @@ fn is_beta_prerelease(version: &Version) -> bool { /// Only the matching final is skipped. A genuinely newer final — `0.19.0` while running /// `0.18.0-beta.1` — is still taken, so a node does not stagnate if the beta line stalls. /// +/// `current` is the build the node is *committed to*, which during a staged rollout is the target +/// it has already selected rather than the one it is still running. Without that, a node part-way +/// through its rollout delay for `0.19.0-beta.1` would compare `0.19.0` against the older +/// `0.18.0-beta.1`, find the cores differ, and take the final instead — so whether a node kept its +/// beta identity would depend on where its rollout jitter fell. +/// /// Beta channel only: a node running a beta build while configured for `stable` should land on the /// final, since that is its route back to the stable line. #[must_use] @@ -504,14 +514,20 @@ fn is_redundant_beta_promotion( /// Select the most appropriate upgrade from a list of releases. /// /// Only versions eligible for the channel are considered, and a beta node skips the promotion of -/// the very build it is running; see `version_matches_channel` and `is_redundant_beta_promotion`. +/// the build it is committed to; see `version_matches_channel` and `is_redundant_beta_promotion`. /// /// Returns the newest version that matches the channel and has platform assets. fn select_upgrade_from_releases( releases: &[GitHubRelease], current_version: &Version, channel: UpgradeChannel, + pending_version: Option<&Version>, ) -> Option { + // The build this node is committed to: the staged-rollout target it has already selected if + // there is one, otherwise whatever it is running. Only the redundancy check uses this — the + // "is it newer" guard stays on the running version, so a withdrawn pending release cannot + // strand the node above everything still published. + let committed_version = pending_version.unwrap_or(current_version); let mut best: Option = None; for release in releases { @@ -527,7 +543,7 @@ fn select_upgrade_from_releases( continue; } - if is_redundant_beta_promotion(&version, current_version, channel) { + if is_redundant_beta_promotion(&version, committed_version, channel) { debug!("Skipping {version}: promotion of the beta already running"); continue; } @@ -1112,7 +1128,8 @@ mod tests { ]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable, None) + .unwrap(); assert_eq!(upgrade.version, Version::new(1, 1, 0)); assert!(upgrade.download_url.contains("stable")); } @@ -1163,7 +1180,7 @@ mod tests { ]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(upgrade.version, Version::parse("1.2.0-beta.1").unwrap()); assert!(upgrade.download_url.contains("beta")); } @@ -1207,10 +1224,12 @@ mod tests { ]; let stable = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable, None) + .unwrap(); assert_eq!(stable.version, Version::parse("0.16.0").unwrap()); - let beta = select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + let beta = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(beta.version, Version::parse("0.17.0-beta.1").unwrap()); } @@ -1222,11 +1241,44 @@ mod tests { let releases = vec![release_with_assets("v0.18.0")]; assert!( - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).is_none(), + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).is_none(), "beta node should stay on 0.18.0-beta.1 when only its own promotion is published" ); } + /// A node part-way through its staged rollout for `0.19.0-beta.1` holds that target when the + /// promoted `0.19.0` appears, instead of being retargeted onto the final. + #[test] + fn test_select_upgrade_beta_holds_pending_beta_against_its_promotion() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let pending = Version::parse("0.19.0-beta.1").unwrap(); + let releases = vec![ + release_with_assets("v0.19.0-beta.1"), + release_with_assets("v0.19.0"), + ]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, Some(&pending)) + .unwrap(); + assert_eq!(upgrade.version, Version::parse("0.19.0-beta.1").unwrap()); + } + + /// Without a pending target the same release pair resolves to the final: the node is running + /// `0.18.0-beta.1`, whose core differs from `0.19.0`, so nothing marks the final redundant. + /// This is what makes threading the pending target through necessary. + #[test] + fn test_select_upgrade_beta_without_pending_takes_the_final() { + let current = Version::parse("0.18.0-beta.1").unwrap(); + let releases = vec![ + release_with_assets("v0.19.0-beta.1"), + release_with_assets("v0.19.0"), + ]; + + let upgrade = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); + assert_eq!(upgrade.version, Version::parse("0.19.0").unwrap()); + } + /// The skip is narrow: a genuinely newer final is still taken, so a beta node does not /// stagnate if the beta line stalls. #[test] @@ -1238,7 +1290,7 @@ mod tests { ]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(upgrade.version, Version::parse("0.19.0").unwrap()); } @@ -1253,7 +1305,7 @@ mod tests { ]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(upgrade.version, Version::parse("0.19.0-beta.1").unwrap()); } @@ -1265,7 +1317,8 @@ mod tests { let releases = vec![release_with_assets("v0.18.0")]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Stable, None) + .unwrap(); assert_eq!(upgrade.version, Version::parse("0.18.0").unwrap()); } @@ -1276,7 +1329,7 @@ mod tests { let releases = vec![release_with_assets("v0.18.0-beta.2")]; let upgrade = - select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(upgrade.version, Version::parse("0.18.0-beta.2").unwrap()); } @@ -1290,7 +1343,8 @@ mod tests { release_with_assets("v0.17.0-beta.1"), ]; - let beta = select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta).unwrap(); + let beta = + select_upgrade_from_releases(&releases, ¤t, UpgradeChannel::Beta, None).unwrap(); assert_eq!(beta.version, Version::parse("0.17.0-beta.1").unwrap()); } } From 8020eed391faa4d3b8b6407c9f3635bc20707cb8 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 20 Aug 2026 21:37:02 +0100 Subject: [PATCH 5/6] chore(release): ship rc-2026.8.3 to beta as 0.17.2-beta.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c02513f..97666193 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,7 +809,7 @@ dependencies = [ [[package]] name = "ant-node" -version = "0.17.2-beta.1" +version = "0.17.2-beta.2" dependencies = [ "alloy", "ant-protocol", diff --git a/Cargo.toml b/Cargo.toml index c432d090..c45f2f70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ant-node" -version = "0.17.2-beta.1" +version = "0.17.2-beta.2" edition = "2021" authors = ["David Irvine "] description = "Pure quantum-proof network node for the Autonomi decentralized network" From 308514c0506fdc0773a59f85316d88027c1273f7 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 27 Aug 2026 00:03:31 +0100 Subject: [PATCH 6/6] chore(release): promote rc-2026.8.3 to 0.17.2 --- Cargo.lock | 17 ++++++++++------- Cargo.toml | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97666193..27f5d4ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,7 +809,7 @@ dependencies = [ [[package]] name = "ant-node" -version = "0.17.2-beta.2" +version = "0.17.2" dependencies = [ "alloy", "ant-protocol", @@ -862,8 +862,9 @@ dependencies = [ [[package]] name = "ant-protocol" -version = "2.3.3-rc.1" -source = "git+https://github.com/WithAutonomi/ant-protocol?branch=rc-2026.8.3#d7e994ead6a8be716436313c4b3655a256433045" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a8d4262f20934f579f1f37e7855718d75156e24b3441eabf089e3049d4c831" dependencies = [ "blake3", "bytes", @@ -4919,8 +4920,9 @@ dependencies = [ [[package]] name = "saorsa-core" -version = "0.27.1-rc.1" -source = "git+https://github.com/WithAutonomi/saorsa-core?branch=rc-2026.8.3#b6fb91d0ca2300c1458ce384a21fac199cb3ecb5" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39dc71cd6ca7ec999ad4285a323fb5fbffc1b1c0554ff1dbf77176a68cf1db" dependencies = [ "anyhow", "async-trait", @@ -5034,8 +5036,9 @@ dependencies = [ [[package]] name = "saorsa-transport" -version = "0.36.1-rc.1" -source = "git+https://github.com/WithAutonomi/saorsa-transport?branch=rc-2026.8.3#0fc30febc4fc00bb9e36cf9fc228d79429b5dfc9" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88d73c4afc06975511b3e757e38bb9f57481a1219d013a48ea643f4850e6f7c" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index c45f2f70..780ed96b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ant-node" -version = "0.17.2-beta.2" +version = "0.17.2" edition = "2021" authors = ["David Irvine "] description = "Pure quantum-proof network node for the Autonomi decentralized network" @@ -39,10 +39,10 @@ mimalloc = "0.1" # Until then, the git pin tracks the matching saorsa-core lineage # (the rc-2026.4.2 branch) so Cargo can unify the wire types here # with ant-protocol's re-exports. -ant-protocol = { git = "https://github.com/WithAutonomi/ant-protocol", branch = "rc-2026.8.3" } +ant-protocol = "2.3.3" # Core (provides EVERYTHING: networking, DHT, security, trust, storage) -saorsa-core = { git = "https://github.com/WithAutonomi/saorsa-core", branch = "rc-2026.8.3" } +saorsa-core = "0.27.1" saorsa-pqc = "0.5" # Payment verification - autonomi network lookup + EVM payment