The native workspace and restricted Rust contract profile pin Rust 1.99.0. rust-toolchain.toml installs Clippy, rustfmt, rust-analyzer and the wasm32-unknown-unknown target. On Windows, tools/enter-dev.ps1 puts the rustup proxies ahead of older standalone Rust installations in the current shell.

The 2026-09-29/30 dependency upgrade keeps the canonical wire formats and existing wallet-v1 format. The failing CI logs used AeadInPlace methods removed by chacha20poly1305 0.11; vaults now use AeadInOut with checked fixed-size nonce/tag conversion. A committed vault produced with Argon2 0.5.3 and chacha20poly1305 0.10.1 checks backward compatibility. The seed is a public RFC 8032 test vector, not operator material. Source packages have an explicitly new compiler-profile commitment; old bundle handling.

All direct Cargo dependencies were checked against the crates.io stable releases; both workspace and standalone fuzz lock resolutions were refreshed. Upstream transitive requirements remain authoritative: forcing unrelated major versions through a dependency's API does not establish compatibility. The 2026-09-30 OSV query over the workspace lockfile's 132 registry packages was manual and left no committed artifact. .github/scripts/audit-dependencies.py now performs that lookup reproducibly under an explicit request cap and timeout, covers the workspace-excluded fuzz lockfile as well, and distinguishes a live query from verification against its pinned snapshot; CI runs it from .github/workflows/security.yml. Measured counts, policy results and the per-dependency trust surface are recorded in dependency and security review. A database check is not an independent implementation audit.

Native artifacts

python -B .github/scripts/verify-native-build.py x86_64-pc-windows-msvc --binaries-output target/verified-native
python -B .github/scripts/package-build.py x86_64-pc-windows-msvc --binaries target/verified-native --revision <full-commit-hash>
python -B -m unittest discover -s .github/scripts -p 'test_*.py' -v

The verifier resolves absolute Cargo/rustc paths for rust-toolchain.toml through rustup, sets the exact RUSTC for child builds and removes ambient compiler wrappers. A standalone compiler earlier in PATH cannot replace the pinned one. The verifier builds the complete release workspace twice in separate fresh directories, disables incremental compilation, remaps workspace/build paths, strips debug information and enables the MSVC deterministic-link option. It compares SHA-256 hashes of cli, daemon, cargo-contract and dns. An optional output directory receives binaries only after all comparisons match. target/native-reproducibility.json records compiler identity and binary hashes. Linux uses the same command with x86_64-unknown-linux-gnu.

The archive builder includes those four binaries, documentation and banner, license, README, lockfile and compiler manifest. Files have a sorted order, fixed modes, zero ownership and an explicit timestamp (SOURCE_DATE_EPOCH, default zero). The gzip header contains no host filename or current time. BUILD.json records build identity and every payload's SHA-256; SHA256SUMS commits the complete archive. Regression tests change filesystem timestamps and permissions, check exact equality, inspect all hashes and reject incomplete builds. The packager reads target/native-reproducibility.json (or --build-report), checks the target and all four binary hashes against the exact packaged payloads, and takes compiler identity from that report. A changed binary or stale/mismatched report fails before archive publication. These scripts perform no upload or signing.

CI's Linux/Windows build matrix now runs this two-build gate and packages its verified output. Release-profile tests also invoke all four real Rust-to-WASM SDK/package checks instead of leaving them permanently ignored.

Local evidence

On Windows with Rust 1.98.1 the upgrade passed workspace tests in debug and release, strict Clippy and rustdoc, formatting, all four explicit Rust-to-WASM tests, standalone fuzz-target compilation, the 100,000-input extension mutation campaign, the legacy vault fixture and archive reproducibility tests. Two fresh native builds produced equal hashes for all four executables. Both repositories' workflow files passed actionlint 1.7.12.

The companion web workspace uses Node 26.10.0, npm 12.1.0, Turbo 2.11.5 and Next.js 16.3.7. TypeScript 7.0.2 supplies the native CLI; the supported TypeScript 6.0.2 API package remains available to Next.js/MDX/ESLint. Direct dependency checks report no outdated packages, and npm audit reports no known vulnerabilities. Content checks, nine RPC tests, type checking, lint, all three production builds and 28 desktop/mobile browser scenarios passed. Browser tests used a newly provisioned four-validator TLS devnet plus observer and a real finalized payment.

No hosted CI runs were started. Linux execution and reproducibility on independent machines remain to be observed; equal builds on one Windows host establish only the measured scope. Live rotating consensus was subsequently implemented. Active PoTB, governance, physical retention and alternate runtime qualification retain their own ROADMAP entries.

2026-10-02 stable refresh

A new check of the official stable Rust manifest and both package registries found Rust 1.99.0, wat 1.260.0, npm 12.2.0, Turbo 2.11.6, Next.js 16.3.8 and Node types 26.6.4. Those versions are now pinned; Node itself remains 26.10.0. Both Cargo locks and the web lockfile were refreshed. The 132-package OSV query and npm audit returned no known vulnerabilities on this date. Both were manual; the Cargo side is now reproduced by the committed script described above, while the npm audit has no committed automation.

Rust 1.99 added strict Clippy diagnostics for empty-value assertions. Telemetry and test assertions now pass those gates. The atomic try_update rename remains unstable under atomic_try_update, so saturating counters keep using the stable fetch_update API. Contract builds resolve the pinned compiler once by its absolute rustup path and reuse it for ABI, SDK and both artifact builds. A regression test puts a fake rustc first in PATH and supplies an invalid ambient toolchain while requiring identical executable WASM output.

The refreshed compiler passed the full debug/release workspace suites, strict Clippy/rustdoc, all four real WASM build tests, standalone fuzz-target compilation and the 71-seed million-input deterministic campaign. The 50 existing protocol fixtures remained byte-identical. Web type/lint checks, nine RPC tests and all three production builds passed on the refreshed dependencies.

Two fresh native Rust 1.99.0 builds with explicit compiler paths produced identical SHA-256 hashes for all four executables. Archive regression tests also reject a report naming a different target, compiler, build count or binary payload.

All 28 desktop/mobile browser scenarios also passed against a new four-validator TLS devnet, an observer and a real finalized payment. The local test build sets NEXT_PUBLIC_*_URL origins before compilation, as required by Next.js; runtime-only URL overrides do not rewrite prerendered cross-app links.

2026-10-08 first observed hosted run

Earlier platform claims in this document rest on local Windows checks. On 2026-10-08 the CI workflow ran on GitHub-hosted runners for the first time, so some of that evidence is now observed rather than configured.

Passed: formatting; strict Clippy on ubuntu-latest and windows-latest; and the workspace suite on ubuntu-latest in both the debug and release profiles, which is the first observed Linux suite result. The windows-latest debug suite also passed.

Failed, with causes recorded here rather than summarised away. The documentation job rejected a public doc comment in crates/crypto/src/blake2s.rs that linked an item outside its module scope, under rustdoc::broken-intra-doc-links. The fuzz compile-check and both release-build jobs failed on one shared cause: the workspace-excluded fuzz package's lockfile was ignored by .gitignore, so cargo check --locked could not resolve it and the advisory review's both-lockfile test raised missing lockfile. That lockfile is now committed, because the advisory scope recorded in document 51 is only reproducible when it is.

The windows-latest release suite failed one test, tls_rotating_profile_serves_verifiable_handoffs_and_recovers_all_roles, where a restarted late-joining observer reported no retained state at height 2. The same test passed on the three other matrix legs and passes locally in release. Retention eviction is excluded, since the window is 64 blocks, and log replay rebuilds the index. The trigger was not reproduced in that run, so the assertion was left intact and made diagnostic instead of relaxed.

Second hosted run, 2026-10-09

The second run narrowed the matrix to two failures, both since reproduced locally and root-caused. Formatting, strict Clippy on both platforms, documentation, the fuzz compile-check, both release builds and workflow validation passed, and both release test legs passed with 1 188 and 1 189 tests. Only the two debug legs failed, one test each.

ubuntu-latest debug failed tls_potb_profile_serves_verifiable_handoffs_and_recovers_all_roles on the same assertion as the previous run's Windows failure, which identified the cause the earlier single sighting could not. The payment the test waits for lands in block one, so await_payment returns as soon as a restarted late-joining observer has block one, while that observer may still sit below height 2. The historical query at height 2 then legitimately found nothing. Two separate defects combined: the test used a finalized payment as the precondition for a historical query, which it is not, and the RPC collapsed "this height is not finalized here yet" and "this finalized height left the retained index" into the same null result, so no caller could tell catch-up from absent history. Both are fixed. state_proof_at now reports a height above the served head as Unavailable and reserves null for an evicted finalized height, and both profile tests wait on the answering node's own finalized head. A regression test drives an observer that cannot leave its genesis anchor and asserts the distinction directly.

windows-latest debug failed malformed_forked_incomplete_or_wrong_height_handoffs_never_advance_authority with WSAEWOULDBLOCK while reading a request length. The test peer deliberately makes its listener non-blocking so an accept deadline can be enforced, and on Windows an accepted socket inherits that mode while set_read_timeout does not clear it. The hazard was already known here: DeadlineSocket::new carries the same fix and document 20 documents it. The sweep for the pattern found four affected sites and one of them was production code, not a test: the daemon metrics listener is non-blocking and its accepted stream was read a byte at a time, so on Windows a Prometheus request whose next byte had not arrived was dropped, silently, because the caller discards the error. Its own unit test missed this by binding a blocking listener. All four now clear the mode explicitly.

Neither failure is platform-specific in cause: the state-proof race was observed on Windows release first and Linux debug second, and the socket-mode defect is dormant on Linux only because accept there does not inherit the flag.

Two hosted runs with failures are not cross-platform qualification. This run does establish Linux reproducibility: the ubuntu-latest release-build job passed in full, and that job runs the two-build byte comparison, the smoke checks, the archive and the qualification report in one sequence, so a pass means the two fresh Linux builds produced equal hashes for all four executables. An earlier statement here that Linux reproducibility remained unobserved described the first run, where the release-build jobs failed before their archive step, and was left standing after the second run contradicted it. What the second run does not establish is a clean four-leg suite, which the two debug failures denied, or reproducibility across two machines, which one run cannot supply at all.

Clean four-leg run

The repository owner reports that the CI workflow is now green on every job on GitHub-hosted runners, including all four tests legs, after the two root-caused debug failures above were fixed. The evidence that makes that an observation rather than a report is the run's own artifacts, and the exact figures are recorded here as soon as they are supplied.

To be supplied, and invented by nothing in this repository: the GitHub Actions run identifier <run-id>, its UTC date <date>, and the per-leg counts the four suite-<os>-<profile> artifacts of that run record. For any run that includes the comparison job described below, all of those figures already sit in one astrolune.suite-qualification/1 verdict in that run's suite-qualification artifact — the four legs' passed, failed, ignored, measured, filtered_out and suites, their shared rustc identity, the dev and release count drift, and the advisory platform and profile differences — so the figures are read from one machine-written document rather than retyped from four job logs. A run that predates that job has the four leg artifacts but no verdict, and its figures must be read from them directly.

Until <run-id> and <date> replace those placeholders, this section records a report and not an observation, and the cross-platform suite qualification roadmap item stays open on that basis alone. A clean four-leg run, once recorded, still establishes only that the pinned toolchain's workspace suite passes on two GitHub-hosted runner images in two profiles on one revision. It is not an independent audit, not a result on any platform outside that matrix, not a statement about any revision but that one, and not evidence about hardware, timing or load that a shared hosted runner cannot control.

Four-leg suite comparison

.github/scripts/suite-report.py records each leg, and .github/scripts/compare-suites.py decides whether the four together qualify the revision. The CI job Four-leg suite qualification downloads every suite-* artifact of the run and invokes it, and CI required checks requires that job, so the verdict is a branch gate rather than a figure a reader has to total by hand.

python -B .github/scripts/compare-suites.py --artifact-root target/suite-legs --output target/suite-legs/SUITE-QUALIFICATION.json
python -B .github/scripts/compare-suites.py suite-ubuntu-latest-dev/SUITE.json suite-ubuntu-latest-release/SUITE.json suite-windows-latest-dev/SUITE.json suite-windows-latest-release/SUITE.json

The expected leg set is the four pairs of ubuntu-latest/windows-latest and dev/release, written into the script rather than read from the files it is given, because a set derived from the input can never notice that one of its members never arrived. A comparison that cannot name every expected leg exactly once is an error and emits no verdict at all: an absent leg, a leg supplied twice, and a leg the matrix never declares are each refused by name. Every leg must additionally carry the rustc -Vv host triple its own platform implies, so a result dropped into the wrong artifact directory cannot be counted as the leg its directory name claims.

A suite result carries no digest over its own fields, so the comparer re-derives what it can: a leg whose outcome does not follow from its own failed count and exit_status, or that counts failures it does not name, has been edited and is refused rather than counted. Qualification then requires every leg's outcome to be ok, its exit_status to be zero, its failed count to be zero and its failed_tests to be empty, and requires all four legs to report one rustc version, since all four install the same pinned toolchain and a second compiler identity means one leg qualified something else.

Cross-leg count differences are split by whether the matrix explains them. The two legs of one profile run the same commands over the same workspace, so their passed and suites counts may differ only by platform-gated tests; the bound is 5% of the larger leg, and a larger gap, or a leg reporting zero, is a refusal. Differences the matrix does cause are advisory and reported rather than refused: host differs by platform by construction, and the release legs run a fifth cargo test invocation that filters cargo-contract down to its four ignored tests, so ignored, measured and filtered_out legitimately move with the profile. This mirrors the host and rustc advisory split in compare-qualification.py.

The verdict is astrolune.suite-qualification/1, written with sorted keys and an LF terminator, and it records the per-leg counts, the totals, the drift each profile allowed and observed, the advisory differences, and every refusal in sorted order rather than only the first. It establishes nothing about a revision: a suite result carries no commit hash, so the verdict binds four legs of one run, not four legs of one revision, and the run itself is what names the revision. It is not a coverage measurement, not a timing measurement — the leg records deliberately exclude timings so two runs of one revision on one platform stay byte-identical — and not evidence that the tests are adequate.

Independent-run reproducibility comparison

.github/scripts/compare-qualification.py was committed and unit-tested but reachable from no workflow, so the qualification-<target> artifact's 90-day retention held evidence nothing ever read. The CI job Independent-run reproducibility (<target>) now closes that: it downloads this run's QUALIFICATION.json, asks the Actions API for the most recent other successful CI run of the same head_sha, downloads that run's retained report for the same target, and compares the two. CI required checks requires the job, so a disagreement blocks rather than sits in an artifact.

What the comparator requires is that the two reports name one target, carry digests their own fields reproduce, agree on every field in COMPARABLE, and come from distinct machine identities. ci.yml sets ASTROLUNE_MACHINE to ${{ runner.name }}#${{ github.run_id }}.${{ github.run_attempt }}, and the run identifier differs between any two runs, so two runs of one revision satisfy the distinctness requirement by construction. That is worth stating plainly rather than presenting as a stronger result than it is. On GitHub-hosted runners each job receives a freshly provisioned ephemeral virtual machine, so two runs are in practice two machines; runner.name is a pool label such as GitHub Actions 2 and is not a hardware identity, so the check cannot tell a second machine from a second run on the same one. Two hosted runners of one image are therefore two machines in exactly the sense the comparator checks: stronger than one run, because the build ran twice on separately provisioned hosts with independently populated caches, and weaker than two independently administered hosts, because both are the same image, the same operating-system patch level, the same filesystem layout and the same administrator. Byte identity across two differently administered machines, across two base images, or across any host outside GitHub's hosted pool is not established, and the independent_builds count inside one report remains two builds on one machine.

The comparison is opportunistic by design. A revision whose CI has run once has no second report, and a revision whose earlier run is older than the 90-day retention has no retained artifact; the job reports either case in its log and exits zero, because evidence that is unavailable is not evidence of agreement. Pull-request runs are excluded from the candidates, and the two reports' own revision fields are compared before the comparator is invoked, because on a pull-request run GITHUB_SHA is a merge commit that moves with the base branch: two correct builds of one head commit under two different merge commits would otherwise be reported as a reproducibility failure. A reader therefore cannot infer from a green job that two machines were compared, only that no two available reports disagreed. The job performs no signing, holds no key, and publishes nothing.

Источник: docs/39-toolchain-and-release-qualification.md
Отдельная MDX-копия · SHA-256 исходника: b4ca78154e051744bacb94bd15ed53e1f0fae7e9d519bfedff534b39d432d0e5