diff --git a/.github/workflows/wai-web-reach.yml b/.github/workflows/wai-web-reach.yml new file mode 100644 index 000000000..6e7c6fcac --- /dev/null +++ b/.github/workflows/wai-web-reach.yml @@ -0,0 +1,220 @@ +name: wai-web reach builds + +# The reach builds of wai-web (wai/wai-web/reach/): the shipped build, L1 (legalised at +# the JS interface), L2 (MVP-lowered and legalised) and J (the wasm2js translation), +# each run on an engine line it serves and compared with the shipped build output for +# output. +# +# On the linux runner, after a gate that reads the pull request's own diff, one leg per +# package: +# +# record the package of record (reach/record/: the two files the site serves under +# /wai/pkg/, built from the commit reach/manifest.json names). Both files must +# have the manifest's SHA-256, and build.sh runs with WAI_REACH_REQUIRE_RECORD=1, +# so every output must have the manifest's SHA-256 and size: the bytes the site +# serves under /wai/pkg/reach/. The package is kept, not rebuilt here: the same +# commit and recipe give different wasm on a Linux x86_64 host than on the +# macOS arm64 host that built it (the glue is the same). +# tree wai-web built from this tree, so a change to wai-web meets the reach builds +# before the package of record moves to it. +# +# Each leg then: +# +# 1. derives every reach build with reach/build.sh (pinned binaryen and JS tools; J's +# wasm2js is binaryen's WebAssembly build, the same on every host), which checks that +# the translation and the glue parse as ECMAScript 5 and writes each output's SHA-256 +# to a manifest; +# 2. on current Node (reach/test/run-modern.mjs): the loader must choose the shipped +# build; every rung and profile, forced, must give the shipped build's outputs over +# the corpora; an engine stripped to ES5 must be given J and the same outputs; and +# the demo verify scripts must pass against shipped, L1, L2 and J; +# 3. on older engine lines, each in its own Node image (reach/test/conformance.js, +# which is ECMAScript 5): the loader must choose the rung that line needs, by +# feature probes alone, and every workload must equal the shipped build's output +# from step 2: +# node:0.12 no WebAssembly -> J +# node:8.0.0 WebAssembly without the RegExp dotAll flag -> J (full), L2 (receipt) +# node:8 MVP WebAssembly, no BigInt -> L2 +# node:10 MVP WebAssembly, BigInt -> L2 +# node:12 post-MVP features, no BigInt at the boundary -> L1 +# node:14 post-MVP features, no BigInt at the boundary -> L1 +# These are proxies: engine lines that real devices also ship (the README says +# which device classes each stands for). No real old device runs here. +# +# The images run as `docker://` steps: each mounts this job's workspace, so the build +# and the reference of steps 1-2 are read where they were written. They run no +# JavaScript action, since their Node is too old to. +# +# Push filters match the push's delta, so every path a step reads is listed. + +# One run per branch at a time. A push supersedes the run before it; on main a newer +# merge's tree contains the older one's. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-web/**' + - 'wai/wai-rs/**' + - 'wai/wai-quantum/**' + - 'wai/neural-conformance/**' + - 'wai/staged-conformance/**' + - 'wai/staged-measure-conformance/**' + - 'wai/registry/**' + - '.github/workflows/wai-web-reach.yml' + pull_request: + paths: + - 'wai/wai-web/**' + - 'wai/wai-rs/**' + - 'wai/wai-quantum/**' + - 'wai/neural-conformance/**' + - 'wai/staged-conformance/**' + - 'wai/staged-measure-conformance/**' + - 'wai/registry/**' + - '.github/workflows/wai-web-reach.yml' + +env: + REACH: wai/wai-web/reach + +jobs: + # Forgejo matches `paths:` against the push delta, so a rebase fires this workflow + # whatever the pull request changes. This job reads the pull request's own diff + # against the paths above (scripts/pr-touches-paths.py, which fails open) and the + # build runs only when it touches them. On a push to main it answers `true`. + relevant: + name: wai-web reach · does this change touch it + runs-on: linux + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-web-reach.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the reach builds: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + reach: + name: wai-web reach builds · ${{ matrix.name }} + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - leg: record + name: the package of record, every output as recorded + - leg: tree + name: this tree's wai-web, every rung on its engine line + env: + DIST: wai/wai-web/reach/ci-dist-${{ matrix.leg }} + steps: + - uses: actions/checkout@v5 + # The docker runner image ships no Rust. + - name: Install Rust 1.98.0 with the wasm32 target + if: matrix.leg == 'tree' + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32-unknown-unknown --toolchain 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the crate + # must match exactly. + - name: wasm-bindgen CLI 0.2.127 + if: matrix.leg == 'tree' + shell: bash + run: cargo install --locked wasm-bindgen-cli --version 0.2.127 + - uses: actions/setup-node@v4 + with: + node-version: '26' + # The site's package is wasm-pack's web target with wasm-opt off + # ([package.metadata.wasm-pack.profile.release]): cargo, then wasm-bindgen. It goes + # outside the tree, where no package.json decides how Node reads the glue. + - name: Build wai-web for wasm32 from this tree (web target) + if: matrix.leg == 'tree' + shell: bash + working-directory: wai/wai-web + run: | + set -euo pipefail + cargo build --locked --release --target wasm32-unknown-unknown + wasm-bindgen --target web --out-dir "$RUNNER_TEMP/pkg" --out-name wai_web \ + target/wasm32-unknown-unknown/release/wai_web.wasm + # The package of record leaves the tree for the same reason, after its check. + - name: The package of record (reach/record/), held to the manifest + if: matrix.leg == 'record' + shell: bash + run: | + set -euo pipefail + node -e ' + const fs = require("fs"), path = require("path"), crypto = require("crypto"); + const [man, pkg] = process.argv.slice(1); + const m = JSON.parse(fs.readFileSync(man, "utf8")); + console.log(`the package of record: ${m.input.source}`); + let bad = 0; + for (const [n, e] of Object.entries(m.input.files)) { + const b = fs.readFileSync(path.join(pkg, n)); + const got = crypto.createHash("sha256").update(b).digest("hex"); + console.log(`${got} ${n} (${b.length} B); the manifest records ${e.sha256} (${e.bytes} B)`); + if (got !== e.sha256 || b.length !== e.bytes) bad++; + } + if (bad) { console.error(`reach: reach/record/ is not the package the manifest names (${bad} file(s) differ)`); process.exit(1); }' \ + "$REACH/manifest.json" "$REACH/record" + mkdir -p "$RUNNER_TEMP/pkg" + cp "$REACH/record/wai_web_bg.wasm" "$REACH/record/wai_web.js" "$RUNNER_TEMP/pkg/" + # The build and the reference stay in the workspace: the engine images below + # mount it, and nothing else. + - name: Reach builds (reach/build.sh, pinned binaryen and JS tools) + shell: bash + run: | + set -euo pipefail + if [ "${{ matrix.leg }}" = record ]; then export WAI_REACH_REQUIRE_RECORD=1; fi + WAI_REACH_TOOLS="$RUNNER_TEMP/reach-tools" bash "$REACH/build.sh" "$RUNNER_TEMP/pkg" "$DIST" + cat "$DIST/manifest.json" + - name: Current Node — every rung forced, the demo verify scripts on every full build + shell: bash + run: node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern-${{ matrix.leg }}" + + - name: node:0.12 (no WebAssembly) — J chosen and equal to the shipped build + uses: docker://node:0.12 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; for p in full verify_decode verify receipt; do node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile $p --expect-rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; done" + - name: node:8.0.0 (WebAssembly, no RegExp dotAll) — J for the full build, the L2 receipt build + uses: docker://node:8.0.0 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile receipt --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json" + - name: node:8 (MVP WebAssembly, no BigInt) — L2 chosen and equal to the shipped build + uses: docker://node:8 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; for p in full verify_decode verify receipt; do node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile $p --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; done; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json" + - name: node:10 (MVP WebAssembly, BigInt) — L2 chosen and equal to the shipped build + uses: docker://node:10 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json" + - name: node:12 (post-MVP WebAssembly, no BigInt at the boundary) — L1 chosen and equal to the shipped build + uses: docker://node:12 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json" + - name: node:14 (post-MVP WebAssembly, no BigInt at the boundary) — L1 chosen and equal to the shipped build + uses: docker://node:14 + with: + entrypoint: /bin/sh + args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json" diff --git a/wai/.gitignore b/wai/.gitignore index 2b99657fc..225b95c31 100644 --- a/wai/.gitignore +++ b/wai/.gitignore @@ -57,3 +57,8 @@ wai-web/demo/int_hyper/ # Reference codec tools built by tools/build_reference_decoders.sh (sink decoders, fetched out-of-band). /.reference-tools/ +# The reach build's pinned tools and its output (wai-web/reach/build.sh). +wai-web/reach/.tools/ +wai-web/reach/dist/ +wai-web/reach/ci-dist*/ +wai-web/reach/ci-modern*/ diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index f5ea7ee7d..cdf7dff30 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,62 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### wai-web on older engines: the reach builds + +- **Three builds from the shipped package, and a loader** ([wai-web/reach](wai-web/reach/README.md)). + `reach/build.sh` derives them from `wai_web_bg.wasm` and `wai_web.js` with no change to + the Rust: + - **L1** legalises the binary's JS interface (`wasm-opt --legalize-js-interface`), so + engines where i64 does not cross as `BigInt` run the full module; + - **L2** lowers it to MVP WebAssembly and legalises it; + - **J** translates the MVP module to ES5 JavaScript with `wasm2js`. + + L2 and J come in profiles, kept by `wasm-metadce` from the exports each needs: `full`, + `verify_decode`, `verify` and `receipt`. An ES5 runtime supplies the missing builtins + and joins and splits i64 values at the boundary: a `BigInt` where the engine has one, a + `Number` (exact below 2^53) where it does not. The glue is the shipped glue compiled to + ES5, with counted changes. One of them rebuilds a memory view whenever the memory's + buffer changes, not only when it is detached. +- **`wai-load.js` chooses by feature probes alone.** Tiny modules passed to + `WebAssembly.validate` (MVP, sign-extension, non-trapping float-to-int, bulk memory), one + instantiated module that returns its i64 argument, and checks for typed arrays decide + between shipped, L1, L2 and J. The probes never throw. An early WebAssembly engine line + (WebAssembly and the stack-trace capture API, but no RegExp `dotAll` flag: the feature + `noDotAll`) gets J, or the L2 receipt build, because the full MVP module has crashed an + engine of that line on arm64. A rung can be forced, and the loader says why it chose. +- **Pinned and recorded.** binaryen `version_123` is checked against each platform + archive's SHA-256, and TypeScript, esbuild, acorn and binaryen's WebAssembly build are + pinned by `package-lock.json`; J's `wasm2js` is that WebAssembly build, so J comes out + the same on every host. + `reach/manifest.json` records the SHA-256 and sizes of every output built from the + package the site serves, and the commit that package was built from; `reach/record/` + keeps that package. Given it, the build refuses any output that differs. +- **Checked output for output.** + - `reach/test/conformance.js` (ES5) runs workloads over the repository's corpora on any + rung, and every output must equal the shipped build's. The workloads cover receipts, + the staged corpus, the integer payloads, int_synth, the menu, the output tree and the + i64 boundary. + - `reach/test/run-modern.mjs` forces every rung and profile on a current Node, and runs + the demo verify scripts against each full build. + - CI (`wai-web-reach.yml`) runs a leg per package. On the package of record, kept in + `reach/record/` and held to the manifest's SHA-256, every output must equal the + manifest's; on the tree's own wai-web, the reach builds must still build and pass. + Each leg runs the conformance on the Node images of the engine lines each rung serves + (J on `node:0.12`, the early-engine routing on `node:8.0.0`, L2 on `node:8` and + `node:10`, L1 on `node:12` and `node:14`) and the shipped build on a current Node. + + The images stand in for device classes, which the README names by capability. No real + old device has been run. +- **The receipt vector's signer id is a label.** The receipt workloads verify an asset and + its provenance receipt in `reach/test/vectors/provenance/`, signed with `wai_prov`'s + published test seed under its label `did:example:wai-prov`. `wai_prov --carry + ` measures nothing: it carries each step's figure, and its label, from an + earlier receipt for the same lineage and signs them under this revision's id, and it + refuses a receipt that does not verify or whose steps are not this run's. The vector + carries the figures `wai_prov` measured when it first wrote the asset, and the + documented command, carrying the receipt onto itself, writes both files again byte for + byte. + ### Generated pictures and scores, made natively and checked by SHA-256 - **The payload of `wai.generate.spectral`** (SPEC §5, *Payload of diff --git a/wai/README.md b/wai/README.md index b09cdf610..dbc96a7ca 100644 --- a/wai/README.md +++ b/wai/README.md @@ -80,6 +80,31 @@ into: - `wai_envelope_{pack,unpack}` — wrap/unwrap the WAI container - `wai_buffer_free`, `wai_last_error` — memory + error handling +## In the browser, on older engines: the reach builds + +`wai-web` is the same Rust compiled to wasm32 for a page. Its shipped package needs +WebAssembly with sign-extension, non-trapping float-to-int and bulk memory, and i64 +values crossing the JS boundary as `BigInt`. The reach builds +([`wai-web/reach/`](wai-web/reach/README.md)) run that code where those are missing, and +give the same outputs byte for byte: + +| rung | build | for engines with | +|---|---|---| +| shipped | the package | every feature above | +| L1 | the shipped binary, legalised at the JS interface | the same WebAssembly, no `BigInt` at the boundary | +| L2 | MVP-lowered and legalised | MVP WebAssembly | +| J | the wasm2js translation, ES5 | ES5 and typed arrays, no WebAssembly | + +L2 and J also come in reduced profiles: `receipt`, `verify` and `verify_decode`. +`wai-load.js`, an ES5 loader, picks the rung from feature probes alone and says why. A +rung can be forced for testing. `reach/build.sh` makes every build with pinned tools +and records each output's SHA-256 in `reach/manifest.json`. + +CI runs each rung on a Node image of the engine line it serves, and every output must +equal the shipped build's; the reach builds made from the package of record (kept in +`reach/record/`) must equal `reach/manifest.json`. The images stand in for device classes; +no real old device has been run. + ## Registered capabilities (the zeroth menu) | Capability | Codec | Lossless? | Mandatory? | diff --git a/wai/extensions/provenance.md b/wai/extensions/provenance.md index 663a9e169..7970fa48b 100644 --- a/wai/extensions/provenance.md +++ b/wai/extensions/provenance.md @@ -85,7 +85,10 @@ and its figure is unlabelled. `wai_prov` labels each encode it measures `OnChipCounter`, with its declared uncertainty, and records an encode whose measurement fails the §4 ratio or the power reader's liveness (§3.1) as unmetered, and writes a measurement report for each step it measured -(energy-measurement §6). `total_joules_micro` carries no class of its own, and a +(energy-measurement §6). With `--carry` it measures nothing: it carries each +figure, and its label, from an earlier receipt for the same lineage, which must +verify and whose steps must have the same operations, hashes and work, and writes +no report. `total_joules_micro` carries no class of its own, and a total over steps that include an unmetered one covers only the measured steps (`Partial`). diff --git a/wai/wai-rs/src/bin/wai_prov.rs b/wai/wai-rs/src/bin/wai_prov.rs index ac5109abb..45948b619 100644 --- a/wai/wai-rs/src/bin/wai_prov.rs +++ b/wai/wai-rs/src/bin/wai_prov.rs @@ -9,7 +9,7 @@ //! unmetered: the step's `joules_micro` carries the unmetered marker `0`, with //! no label, never a figure of zero joules. //! -//! wai_prov [--no-report] [--previous-report ] +//! wai_prov [--no-report] [--previous-report ] [--carry ] //! //! Writes `/captured.jpeg.wai` (the content-addressed asset), //! `/captured.receipt.json` (the signed provenance), and for each @@ -17,7 +17,18 @@ //! report (energy-measurement §6), a `measurement` claim signed with the //! receipt's key naming the step by its index, the first chained to //! `--previous-report` when it is given and the second to the first. -//! `--no-report` turns the reports off, and says so. The asset is +//! `--no-report` turns the reports off, and says so. +//! +//! `--carry ` measures nothing: each step's figure, and its label +//! if it has one, is carried as an earlier receipt for this same lineage states +//! it, and signed under this revision's signer id. That receipt must verify, and +//! its steps must be this run's — the same operations, input and output hashes +//! and work — so a figure is only ever carried onto the bytes it was measured +//! on. No report is written: a report restates a bracket, and this run took +//! none. Nothing in such a run varies, so a committed asset and receipt +//! (`wai-web/reach/test/vectors/provenance/`) are written again byte for byte +//! when the receipt is carried onto itself; a host whose synthesis or encoders +//! give other bytes has another lineage, and is refused. The asset is //! synthesized deterministically here — WAI is asset-agnostic; the lineage //! mechanism is identical whether the origin is a camera or a synthesizer, //! and that is stated plainly. The point is provenance for bytes WAI @@ -38,9 +49,12 @@ const W: u32 = 512; const H: u32 = 512; fn main() { - let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::>()); + let mut argv: Vec = std::env::args().collect(); + let carry = take_carry(&mut argv); + let report_args = meter_report::parse_args_or_exit(&argv); let args = &report_args.positional; let dir = Path::new(args.get(1).map(String::as_str).unwrap_or(".")); + let carried = carry.as_deref().map(read_carried); std::fs::create_dir_all(dir).expect("mkdir"); // A deterministic synthesized frame (radial rings + gradient). Real @@ -49,17 +63,23 @@ fn main() { let pixels = (W * H) as u64; let raw_hash = blake3::hash(&rgb).into(); - // step 1: lossless PNG master — measured. - let (png_m, png_v) = measure("encode.png", &rgb, |r| { - let _ = std::hint::black_box(png_encode(r, H, W)); - }); + // step 1: lossless PNG master — measured, unless the figures are carried. + let (png_m, png_v) = match carried { + Some(_) => (None, None), + None => measure("encode.png", &rgb, |r| { + let _ = std::hint::black_box(png_encode(r, H, W)); + }), + }; let png = png_encode(&rgb, H, W).expect("png"); let png_hash: [u8; 32] = blake3::hash(&png).into(); - // step 2: published JPEG (from the master pixels) — measured. - let (jpeg_m, jpeg_v) = measure("encode.jpeg", &rgb, |r| { - let _ = std::hint::black_box(jpeg_encode(r, H, W, 90)); - }); + // step 2: published JPEG (from the master pixels) — measured, unless carried. + let (jpeg_m, jpeg_v) = match carried { + Some(_) => (None, None), + None => measure("encode.jpeg", &rgb, |r| { + let _ = std::hint::black_box(jpeg_encode(r, H, W, 90)); + }), + }; let jpeg = jpeg_encode(&rgb, H, W, 90).expect("jpeg"); // Wrap the JPEG as a content-addressed wai.image.jpeg envelope. @@ -77,11 +97,14 @@ fn main() { None => s, } }; - let steps = vec![ + let mut steps = vec![ ProvStep::new("origin.synthesize", [0u8; 32], raw_hash, pixels, 0), step("encode.png", raw_hash, png_hash, png_m.clone()), step("encode.jpeg", png_hash, asset_hash, jpeg_m.clone()), ]; + if let (Some(from), Some(path)) = (&carried, &carry) { + steps = carry_figures(path, from, steps, asset_hash); + } let signer = SigningKey::from_bytes(&[0x70u8; 32]); // 'p' for prov // A demo signer whose seed is published: a label, never origin-bound (jwp-receipts §7.1, §7.8). @@ -106,6 +129,16 @@ fn main() { // The origin step is never metered, so the total covers the measured steps // only, and an all-unmetered lineage is UNMETERED, never 0 µJ. println!("captured.receipt.json : {} steps, total {}, receipt {}…", receipt.steps.len(), receipt.total_label(), &hex(&receipt.receipt_hash())[..16]); + if let Some(path) = &carry { + let shown = |s: &ProvStep| match (s.joules_micro, &s.energy_provenance) { + (0, _) => "UNMETERED".to_string(), + (uj, Some(c)) => format!("{uj} µJ ({})", c.label()), + (uj, None) => format!("{uj} µJ (unlabelled)"), + }; + println!(" encode.png : {} encode.jpeg : {} (carried from {path})", shown(&receipt.steps[1]), shown(&receipt.steps[2])); + println!("report : not written — the figures are carried, and this run took no bracket for a report to restate"); + return; + } let shown = |m: &Option<(u64, EnergyProvenance)>| match m { Some((uj, c)) => format!("{uj} µJ ({})", c.label()), None => "UNMETERED".to_string(), @@ -127,6 +160,54 @@ fn main() { } } +/// Take `--carry ` out of `argv`. `--carry` with no file after it, or +/// given twice, is a usage error (exit status 2), never a measured run. +fn take_carry(argv: &mut Vec) -> Option { + let mut found = None; + while let Some(i) = argv.iter().position(|a| a == "--carry") { + if found.is_some() || !argv.get(i + 1).is_some_and(|p| !p.starts_with("--")) { + eprintln!("usage error: --carry takes one earlier receipt's file, once"); + std::process::exit(2); + } + found = Some(argv.remove(i + 1)); + argv.remove(i); + } + found +} + +/// The receipt `--carry` names. It must parse and verify. +fn read_carried(path: &str) -> AssetReceipt { + let json = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}")); + let r = AssetReceipt::from_json(&json).unwrap_or_else(|| panic!("{path}: not an asset-provenance receipt")); + assert!(r.verify(), "{path}: the receipt to carry from does not verify"); + r +} + +/// This run's steps, each with the figure and the label `from` states for it. +/// Refused (exit status 1) unless `from` is a receipt for this lineage: the same +/// asset, media, capability and group, no parent, and step for step the same +/// operation, input and output hashes and work. +fn carry_figures(path: &str, from: &AssetReceipt, steps: Vec, asset_hash: [u8; 32]) -> Vec { + let same_asset = from.asset_hash == asset_hash + && from.media == "image" + && from.capability == "wai.image.jpeg" + && from.group_id == 0 + && from.parent_receipt_hash.is_none(); + let same_steps = from.steps.len() == steps.len() + && from.steps.iter().zip(&steps).all(|(c, s)| { + c.op == s.op && c.input_hash == s.input_hash && c.output_hash == s.output_hash && c.work == s.work + }); + if !same_asset || !same_steps { + eprintln!("{path}: not a receipt for this lineage (its asset, or its steps' operations, hashes or work, differ from this run's); nothing carried"); + std::process::exit(1); + } + steps + .into_iter() + .zip(&from.steps) + .map(|(s, c)| ProvStep { joules_micro: c.joules_micro, energy_provenance: c.energy_provenance.clone(), ..s }) + .collect() +} + /// Deterministic 512×512 RGB: radial rings over a diagonal gradient. fn synth_rgb() -> Vec { let mut out = Vec::with_capacity((W * H * 3) as usize); diff --git a/wai/wai-web/reach/README.md b/wai/wai-web/reach/README.md new file mode 100644 index 000000000..1258d4883 --- /dev/null +++ b/wai/wai-web/reach/README.md @@ -0,0 +1,222 @@ +# wai-web reach builds + +The shipped wai-web package (`wai_web.js` and `wai_web_bg.wasm`, the wasm-bindgen web +target) runs where an engine has WebAssembly with sign-extension, non-trapping +float-to-int conversion and bulk memory, where a 64-bit integer crosses the JS boundary as +a `BigInt`, and where ES modules load. The reach builds run the same code on engines +without those features, and give the same outputs byte for byte. Nothing in the Rust +changes: every reach build is derived from the shipped package itself. + +| rung | files | the engine needs | the CI proxies it runs on | +|---|---|---|---| +| shipped | `../wai_web.js`, `../wai_web_bg.wasm` (the package) | WebAssembly with sign-extension, non-trapping float-to-int and bulk memory; i64 as `BigInt`; ES modules | a current Node | +| **L1** | `wai_web_bg.l1.wasm` + `wai_web.reach.js` | the same WebAssembly features; no `BigInt` at the boundary | `node:12`, `node:14` | +| **L2** | `wai_web_bg.l2..wasm` + `wai_web.reach.js` | MVP WebAssembly | `node:8.0.0` (receipt), `node:8`, `node:10` | +| **J** | `wai_web.j..js` | ES5 and typed arrays; no WebAssembly | `node:0.12`, `node:8.0.0` (full), and a current Node stripped to ES5 | + +- **L1** is the shipped binary with its JS interface legalised (`wasm-opt + --legalize-js-interface`): an i64 parameter becomes a pair of i32 halves, and an i64 + result returns its low half and passes the high half through `env.setTempRet0`. Nothing + else changes, so the code runs at the shipped build's speed. +- **L2** is the shipped binary lowered to the MVP (`--signext-lowering`, + `--llvm-nontrapping-fptoint-lowering`, `--llvm-memory-copy-fill-lowering`) and then + legalised. Lowered without legalisation it would still fail every export with an i64 in + its signature, so it is never shipped alone. +- **J** is the MVP-lowered module translated to JavaScript by `wasm2js`, in one ES5 + script per profile: the runtime, the i64 table, the translation and the glue. + +`wai_web.reach.js` and every J script carry the same ES5 parts: + +- **The runtime** (`src/wai-reach-runtime.js`) installs what an old engine lacks and the + glue or the translation uses (`Math.imul` / `fround` / `clz32` / `trunc`, + `TypedArray#slice`, `TextEncoder` / `TextDecoder`, `Array#fill`, a small `Map`, a + `Symbol.iterator` stand-in, and a few more), each only where it is missing. It also joins + and splits i64 values at the boundary, from the table the build extracts from the binary. + With `BigInt` a 64-bit value is a `BigInt`, as the shipped build gives it. Without + `BigInt` it is a `Number`, exact below 2^53. +- **The glue** is the shipped `wai_web.js`, changed in a fixed, counted set of places + (`tools/cli.mjs`, `glue`; `manifest.json` records the counts) and compiled to ES5: + - its WebAssembly loading is replaced by two hooks the loader calls; + - a cached memory view is rebuilt whenever `wasm.memory.buffer` is a different buffer. + The shipped glue rebuilt one only when its buffer was detached, which a WebAssembly + memory does when it grows and the translation's memory does not; + - `Symbol.dispose` is read only where `Symbol` exists; + - the 64-bit helpers take the `Number` path without `BigInt`; + - copies out of wasm memory no longer use `TypedArray#slice`, which early engines + implement as a view. + +## Profiles + +L2 and J come in profiles. `profiles.json` lists the exports each one keeps, and +`wasm-metadce` drops what they cannot reach: + +| profile | what it keeps | +|---|---| +| `full` | everything | +| `verify_decode` | `verify`, plus the integer decoders (WIV1, WIH1, WIA1, WIS1, int_synth, the JSON integer forms) and the staged-delivery sink | +| `verify` | `receipt`, plus multi-rendition envelopes, pins, the capability registry, stage identities, session claims and the output tree | +| `receipt` | envelope parsing, asset provenance (content hash, Ed25519 signature, Merkle chain) and the hashes | + +A page that calls a function outside its profile gets a `TypeError`. The shipped build and +L1 are always the full module. + +## The loader + +`wai-load.js` is ES5. It chooses a rung from feature probes alone, never from the user +agent, and its detection never throws: + +1. no typed arrays: none, and the reason says to serve the classical floor renditions; +2. no WebAssembly, or a WebAssembly that refuses a minimal MVP module: **J**; +3. an early WebAssembly engine line (WebAssembly and `Error.captureStackTrace` present, no + RegExp `dotAll` flag; the feature `noDotAll`): **J**, or the L2 receipt build when the + page asks for the receipt profile. The full MVP module has crashed an engine of this + line on arm64; +4. a probe module for sign-extension, non-trapping float-to-int or bulk memory (with a + data-count section) fails `WebAssembly.validate`: **L2**; +5. a one-function module returning its i64 argument does not take and give a `BigInt`: + **L1**; +6. otherwise: **shipped**. + +```html + + +``` + +| option | meaning | +|---|---| +| `base` | the folder of the reach files, ending in `/`; by default the folder `wai-load.js` came from | +| `profile` | `full` (default), `verify_decode`, `verify` or `receipt`; used by L2 and J | +| `force` | `shipped`, `L1`, `L2` or `J`: load that rung whatever the probes say (testing). The global `WAI_REACH_FORCE` does the same | +| `shipped` | `{ glue, wasm }` URLs of the shipped package; by default `base + '../wai_web.js'` and `base + '../wai_web_bg.wasm'` | +| `host` | `{ loadScript, fetchBytes, importModule, streams }`, to load from somewhere other than the page. With `streams` (a browser's default), the shipped glue gets the wasm URL and compiles it while it downloads | + +`WaiLoad.detect(opts)` returns `{ rung, profile, reason, features }` without loading +anything. After a load, `WaiLoad.state` holds what the callback received. + +## Building + +``` +bash wai/wai-web/reach/build.sh [] +``` + +`` holds `wai_web.js` and `wai_web_bg.wasm`. The output goes to +`` (default `reach/dist/`, which git ignores) with a `manifest.json`: every +file's SHA-256 and its raw, gzip and brotli sizes, the input's SHA-256, the tool pins, and +the counts of the glue changes. + +The tools are pinned: + +- binaryen at release `version_123`, by the SHA-256 of each platform's archive, checked + before use. The platforms are macOS arm64 and x86_64, and Linux x86_64 and aarch64; +- TypeScript, esbuild, acorn and binaryen's WebAssembly build of the same release by + `package-lock.json`. `npm ci` checks their integrity hashes. J's `wasm2js` is that + WebAssembly build, run on Node: the x86_64 Linux archive's native `wasm2js` allocates the + translation's locals differently from the macOS archive's, while the WebAssembly build + gives the same output on every host (and the macOS archive's output). Every other step + gives the same bytes from every archive. + +The build refuses: + +- an input that uses any feature beyond the shipped build's; +- a reduced module that exports anything other than its profile; +- an L2 module whose i64 boundary differs from the shipped binary's; +- a glue whose layout it does not recognise; +- a script that does not parse as ECMAScript 5. + +`manifest.json` in this folder is the build of the shipped package of record. Its `input` +says which package: the SHA-256 of both files, the commit they were built from, and how. +Given that package, `build.sh` refuses any output whose SHA-256 differs from the +manifest's; with `WAI_REACH_REQUIRE_RECORD=1` it also refuses any other package. +`WAI_REACH_RECORD=1` writes a new manifest. + +## Conformance + +Every rung must give the shipped build's outputs. The runners below check this: + +- **`test/conformance.js`** (ES5, Node 0.12 onward) loads a rung through `wai-load.js` and + runs `test/workloads.js` over the repository's corpora. Each workload is checked against + an independent reference (a corpus's hashes, a sidecar's SHA-256, the receipt's signed + fields, the registry), and its output must equal the shipped build's byte for byte. The + workloads, by profile: + - receipt: the hashes; envelope parsing; asset provenance over the vector in + `test/vectors/provenance/` (its README says how it is made), including a tampered + copy and a forged joule total; + - verify: the capability menu against the registry; the staged corpus's content hashes; + multi-rendition envelopes; the output tree of the staged-measurement goldens; + - verify_decode: every committed WIV1, WIH1, WIA1 and WIS1 payload against its sidecar; + the JSON integer forms; the committed int_synth vectors; all of `staged-conformance/` + replayed through `StageSink`; + - full: i64 parameters and results on both sides of 2^32. + + Under J, `WebAssembly` is deleted before anything loads. `--strip es5` also deletes + `BigInt`, `Map`, `Symbol`, `Promise` and the other post-ES5 builtins, and makes + `TypedArray#slice` return a view. +- **`test/run-modern.mjs`** runs on a current Node: + - the loader must choose the shipped build, and that run is the reference; + - every rung and profile is forced and must equal it; + - an engine stripped to ES5 must be given J, and the same outputs; + - the demo verify scripts (`demo/verify_*.mjs`: integer payloads, int_synth, the staged + corpus with its guards and fuzz, the menu, the writers, the staged measurement) run + against shipped, L1, L2 and J through a generated adapter module. The measurement + script's written files must equal the shipped build's. + +``` +node wai/wai-web/reach/test/run-modern.mjs --dist --shipped +node wai/wai-web/reach/test/conformance.js --dist --wai wai --rung J --profile receipt --reference +``` + +CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner) runs a leg per package: + +- **The package of record.** `record/` keeps the two files the site serves under + `/wai/pkg/`, built from the commit `manifest.json` names (`input.commit`; `input.source` + says how). Both files must have the SHA-256 the manifest records, and `build.sh` then + runs with `WAI_REACH_REQUIRE_RECORD=1`, so every output must have the manifest's SHA-256 + and size: these are the bytes the site serves under `/wai/pkg/reach/`. The package is + kept rather than rebuilt in CI: the same commit and recipe give a different wasm on a + Linux x86_64 host than on the macOS arm64 host that built it (the glue is the same), so + the commit's build is checked on that host (the site's `build-wasm-pkg.sh recorded wai`). +- **The tree.** wai-web is built from the pull request's own tree, so a change to wai-web + meets the reach builds before the package of record moves to it. These outputs are not + the recorded ones. + +On each leg: + +1. `run-modern.mjs` runs on the outputs; +2. `conformance.js` runs in one Node image per engine line. On each line the loader must + choose that line's rung by itself, and every output must equal the shipped build's + from step 1. + +### What each proxy stands for + +The images are proxies. Each runs one JavaScript engine line on x86_64 Linux, with the +engine features below; real devices ship engines of the same lines. **No real old device +has been run.** Real devices also differ in ways no proxy reproduces: 32-bit ARM code +generation, engines with the JIT switched off, small memories, and engine families other +than the proxied one. + +| proxy | engine features | rung the loader must choose | device classes it stands for | +|---|---|---|---| +| `node:0.12` | ES5 and typed arrays; no WebAssembly | J | TV sets and boxes whose web engine, or system web view, is frozen at an engine with no WebAssembly, smart TVs of model years 2014–2019 among them; phones whose system web view predates WebAssembly; broadcast-application receivers (HbbTV 2.0.x, ATSC 3.0 A/344). Console browsers that run with the JIT off, and engine families other than the proxied one, are J devices that this proxy does not cover | +| `node:8.0.0` | MVP WebAssembly; no RegExp `dotAll` flag | J, or L2 for the receipt profile | TV sets and boxes whose system web view is frozen at an engine with WebAssembly but no RegExp `dotAll` flag. The full MVP module has crashed an engine of this line on arm64; this x86_64 proxy does not crash, so it checks the choice and the outputs, not the crash | +| `node:8` | MVP WebAssembly and the `dotAll` flag; no `BigInt` | L2 | TV sets and boxes whose system web view is frozen at MVP WebAssembly with no `BigInt`, and TV platforms of model years 2019–2020 | +| `node:10` | MVP WebAssembly and `BigInt`; no sign-extension or bulk memory | L2 | TV sets and boxes whose system web view has `BigInt` but not the post-MVP features the shipped build uses | +| `node:12` | the shipped build's WebAssembly features; i64 does not cross the JS boundary as `BigInt` | L1 | TV platforms of model year 2021 | +| `node:14` | the same | L1 | TV platforms and streaming sticks whose engine has the shipped build's WebAssembly features but passes no i64 as `BigInt`, and phones' system web views of the same line | +| a current Node | every feature, with i64 as `BigInt` | shipped | current engines. Engine families other than the proxied one are not proxied here | + +## Limits + +- J is slower than WebAssembly, and far slower on engines without a JIT. The `receipt` + and `verify` profiles are the ones for small or slow devices. +- Without `BigInt`, a 64-bit value is a `Number`, exact below 2^53. `IntPayloadCost.macs` + (a 128-bit value) and the functions that return a `BigInt64Array` still need `BigInt`. +- A trap in the WebAssembly builds is an `Error('abort')` in J. The translation does not + check memory accesses out of bounds, which trap in WebAssembly. The Rust does not make + such accesses, and the conformance runs compare every output. +- Reduced profiles throw a `TypeError` for any function outside them. diff --git a/wai/wai-web/reach/build.sh b/wai/wai-web/reach/build.sh new file mode 100644 index 000000000..2ecba9667 --- /dev/null +++ b/wai/wai-web/reach/build.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# The reach builds of wai-web, from one wai-web package (wai_web.js + wai_web_bg.wasm, +# the wasm-bindgen web target). +# +# bash wai/wai-web/reach/build.sh [] +# +# Writes into (default wai/wai-web/reach/dist, which git ignores): +# +# wai_web_bg.l1.wasm L1 the shipped binary legalised at the JS interface +# wai_web_bg.l2..wasm L2 MVP-lowered and legalised; full, verify_decode, +# verify and receipt +# wai_web.reach.js ES5 runtime + i64 table + glue, for L1 and L2 +# wai_web.j..js J wasm2js translation in one ES5 script per profile +# wai-load.js the loader +# manifest.json every file's SHA-256 and sizes, the input's SHA-256, +# the tool pins and the glue changes +# +# Reproducibility. The tools are pinned: binaryen by release and by the SHA-256 of each +# platform's archive (checked before use), and TypeScript, esbuild, acorn and binaryen's +# own WebAssembly build by package-lock.json (npm ci checks their integrity hashes). J's +# wasm2js is that WebAssembly build, run on Node: the native wasm2js of the x86_64 Linux +# archive gives the translation's locals a different allocation than the macOS archive +# does, while the WebAssembly build gives the same output on every host (and equals the +# macOS archive's). The same package gives the same bytes on every platform below. When the package is the one reach/manifest.json +# names as its input, the build is refused unless every output's SHA-256 equals the +# manifest's; WAI_REACH_RECORD=1 writes the new manifest there instead. +# +# Environment: +# WAI_REACH_TOOLS where binaryen and the JS tools are installed (default +# wai/wai-web/reach/.tools, which git ignores) +# WAI_REACH_RECORD 1: write /manifest.json to reach/manifest.json +# WAI_REACH_REQUIRE_RECORD +# 1: refuse unless the package is the input reach/manifest.json names, +# so the outputs are always checked against the manifest (CI's leg on +# the package of record) +# WAI_REACH_SOURCE a line saying where the package came from, kept in the manifest +# WAI_REACH_INPUT_COMMIT +# the open-standards commit the package was built from, kept in the +# manifest as input.commit +# +# Needs bash, curl, tar, node >= 18 and npm. Platforms: macOS arm64 and x86_64, Linux +# x86_64 and aarch64. +set -euo pipefail + +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "usage: bash build.sh []" >&2 + exit 2 +fi +REACH="$(cd "$(dirname "$0")" && pwd -P)" +PKG="$(cd "$1" && pwd -P)" +DIST="${2:-$REACH/dist}" +mkdir -p "$DIST" +DIST="$(cd "$DIST" && pwd -P)" +TOOLS="${WAI_REACH_TOOLS:-$REACH/.tools}" +mkdir -p "$TOOLS" +TOOLS="$(cd "$TOOLS" && pwd -P)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +for f in wai_web_bg.wasm wai_web.js; do + [ -f "$PKG/$f" ] || { echo "reach: $PKG has no $f" >&2; exit 1; } +done +for t in curl tar node npm; do + command -v "$t" >/dev/null || { echo "reach: needs $t" >&2; exit 1; } +done +NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" +[ "$NODE_MAJOR" -ge 18 ] || { echo "reach: the build needs node >= 18 (this is $(node --version))" >&2; exit 1; } +sha256() { if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; } + +# ---- binaryen, pinned ------------------------------------------------------------- +BINARYEN=version_123 +case "$(uname -s)-$(uname -m)" in + Darwin-arm64) PLAT=arm64-macos; BIN_SHA=74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b ;; + Darwin-x86_64) PLAT=x86_64-macos; BIN_SHA=cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd ;; + Linux-x86_64) PLAT=x86_64-linux; BIN_SHA=e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe ;; + Linux-aarch64) PLAT=aarch64-linux; BIN_SHA=4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376 ;; + *) echo "reach: no pinned binaryen archive for $(uname -s)-$(uname -m)" >&2; exit 1 ;; +esac +ARCHIVE="binaryen-$BINARYEN-$PLAT.tar.gz" +B="$TOOLS/binaryen-$BINARYEN-$PLAT/bin" +if [ ! -x "$B/wasm-opt" ] || [ "$(cat "$TOOLS/binaryen-$BINARYEN-$PLAT/.sha256" 2>/dev/null)" != "$BIN_SHA" ]; then + rm -rf "$TOOLS/binaryen-$BINARYEN-$PLAT" "$TOOLS/$ARCHIVE" + curl -fsSL --retry 3 -o "$TOOLS/$ARCHIVE" \ + "https://github.com/WebAssembly/binaryen/releases/download/$BINARYEN/$ARCHIVE" # allow-name: the binaryen project's release download + got="$(sha256 "$TOOLS/$ARCHIVE")" + if [ "$got" != "$BIN_SHA" ]; then + echo "reach: $ARCHIVE has SHA-256 $got, not the pinned $BIN_SHA" >&2 + rm -f "$TOOLS/$ARCHIVE" + exit 1 + fi + mkdir -p "$TOOLS/binaryen-$BINARYEN-$PLAT" + tar -xzf "$TOOLS/$ARCHIVE" -C "$TOOLS/binaryen-$BINARYEN-$PLAT" --strip-components=1 + rm -f "$TOOLS/$ARCHIVE" + echo "$BIN_SHA" > "$TOOLS/binaryen-$BINARYEN-$PLAT/.sha256" +fi +"$B/wasm-opt" --version | grep -q "($BINARYEN)" || { echo "reach: $B/wasm-opt is not binaryen $BINARYEN" >&2; exit 1; } + +# ---- TypeScript, esbuild, acorn, pinned by package-lock.json ------------------------ +LOCK_SHA="$(sha256 "$REACH/package-lock.json")" +if [ "$(cat "$TOOLS/js/.lock-sha256" 2>/dev/null)" != "$LOCK_SHA" ] || [ ! -d "$TOOLS/js/node_modules" ]; then + rm -rf "$TOOLS/js" + mkdir -p "$TOOLS/js" + cp "$REACH/package.json" "$REACH/package-lock.json" "$TOOLS/js/" + (cd "$TOOLS/js" && npm ci --ignore-scripts --no-audit --no-fund --loglevel=error) + echo "$LOCK_SHA" > "$TOOLS/js/.lock-sha256" +fi +export WAI_REACH_NODE_MODULES="$TOOLS/js/node_modules" +CLI=(node "$REACH/tools/cli.mjs") +# binaryen's wasm2js compiled to WebAssembly, pinned by package-lock.json: host-independent. +W2J=(node "$TOOLS/js/node_modules/binaryen/bin/wasm2js") +"${W2J[@]}" --version | grep -q "($BINARYEN)" || { echo "reach: the pinned WebAssembly wasm2js is not binaryen $BINARYEN" >&2; exit 1; } + +IN="$PKG/wai_web_bg.wasm" +FEATURES=(--enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext) +LOWER=(--signext-lowering --llvm-nontrapping-fptoint-lowering --llvm-memory-copy-fill-lowering) +PROFILES=(verify_decode verify receipt) +rm -f "$DIST"/wai_web_bg.l1.wasm "$DIST"/wai_web_bg.l2.*.wasm "$DIST"/wai_web.reach.js "$DIST"/wai_web.j.*.js "$DIST"/wai-load.js "$DIST"/manifest.json + +# The input must be exactly the shipped build's feature set: MVP + sign-extension, +# non-trapping float-to-int and bulk memory. A module using anything more (multi-value, +# reference types, SIMD, ...) fails here, before any rung is built from it. +if ! "$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" -o "$WORK/shipped.check.wasm" 2> "$WORK/check.err"; then + cat "$WORK/check.err" >&2 + echo "reach: $IN uses a feature beyond MVP, sign-extension, non-trapping float-to-int and bulk memory" >&2 + exit 1 +fi + +# ---- L1: legalised at the JS interface, nothing lowered ------------------------------ +"$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" --legalize-js-interface -o "$DIST/wai_web_bg.l1.wasm" + +# ---- L2: MVP-lowered, then legalised ------------------------------------------------- +# The lowering runs with the post-MVP features enabled for reading; reading the result +# back with MVP features only is the check that nothing post-MVP is left. +"$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" "${LOWER[@]}" -o "$WORK/mvp.full.wasm" +"$B/wasm-opt" "$WORK/mvp.full.wasm" --mvp-features --legalize-js-interface -o "$DIST/wai_web_bg.l2.full.wasm" +for p in "${PROFILES[@]}"; do + "${CLI[@]}" graph "$WORK/mvp.full.wasm" "$REACH/profiles.json" "$p" "$WORK/graph.$p.json" + "$B/wasm-metadce" "$WORK/mvp.full.wasm" --mvp-features --graph-file "$WORK/graph.$p.json" -o "$WORK/mvp.$p.dce.wasm" > /dev/null + "$B/wasm-opt" "$WORK/mvp.$p.dce.wasm" --mvp-features -Oz -ocimfs=200 -o "$WORK/mvp.$p.wasm" + "${CLI[@]}" exports "$WORK/mvp.$p.wasm" "$WORK/graph.$p.json" + "$B/wasm-opt" "$WORK/mvp.$p.wasm" --mvp-features --legalize-js-interface -o "$DIST/wai_web_bg.l2.$p.wasm" +done + +# ---- the i64 table, the runtime and the glue ---------------------------------------- +"${CLI[@]}" same-sigs "$IN" "$WORK/mvp.full.wasm" +"${CLI[@]}" sigs "$IN" "$WORK/sigs.js" +"${CLI[@]}" minify "$REACH/src/wai-reach-runtime.js" "$WORK/runtime.min.js" +"${CLI[@]}" glue "$PKG/wai_web.js" "$WORK/glue.js" "$WORK/glue-notes.json" +"${CLI[@]}" minify "$WORK/glue.js" "$WORK/glue.min.js" +cat "$WORK/runtime.min.js" "$WORK/sigs.js" "$WORK/glue.min.js" > "$DIST/wai_web.reach.js" + +# ---- J: wasm2js on the MVP-lowered modules ------------------------------------------- +# The translations run side by side (each is single-threaded), then in order. +pids=() +for p in full "${PROFILES[@]}"; do + "${W2J[@]}" "$WORK/mvp.$p.wasm" -Oz -ocimfs=200 -o "$WORK/j.$p.mjs" & pids+=($!) +done +for pid in "${pids[@]}"; do wait "$pid" || { echo "reach: wasm2js failed" >&2; exit 1; }; done +for p in full "${PROFILES[@]}"; do + "${CLI[@]}" factory "$WORK/j.$p.mjs" "$WORK/j.$p.factory.js" + "${CLI[@]}" minify "$WORK/j.$p.factory.js" "$WORK/j.$p.factory.min.js" + cat "$WORK/runtime.min.js" "$WORK/sigs.js" "$WORK/j.$p.factory.min.js" "$WORK/glue.min.js" > "$DIST/wai_web.j.$p.js" +done + +cp "$REACH/src/wai-load.js" "$DIST/wai-load.js" +"${CLI[@]}" es5 "$DIST/wai-load.js" "$DIST/wai_web.reach.js" "$DIST"/wai_web.j.*.js + +# ---- manifest -------------------------------------------------------------------------- +cat > "$WORK/tools.json" < JSON.parse(require("fs").readFileSync(f, "utf8"))); + process.exit(JSON.stringify(rec.input.files) === JSON.stringify(out.input.files) ? 0 : 1);' "$REC" "$DIST/manifest.json"; then + node -e ' + const [rec, out] = process.argv.slice(1).map((f) => JSON.parse(require("fs").readFileSync(f, "utf8"))); + const names = [...new Set([...Object.keys(rec.outputs), ...Object.keys(out.outputs)])].sort(); + let bad = 0; + for (const n of names) { + const a = rec.outputs[n], b = out.outputs[n]; + if (!a || !b || a.sha256 !== b.sha256 || a.bytes !== b.bytes) { bad++; console.error(`reach: ${n}: ${b ? b.sha256 : "missing"}, recorded ${a ? a.sha256 : "nothing"}`); } + } + if (bad) { console.error(`reach: ${bad} outputs differ from reach/manifest.json`); process.exit(1); } + console.log(`reach: all ${names.length} outputs equal reach/manifest.json`);' "$REC" "$DIST/manifest.json" +elif [ "${WAI_REACH_REQUIRE_RECORD:-}" = 1 ]; then + echo "reach: WAI_REACH_REQUIRE_RECORD=1, but the package is not the input reach/manifest.json names" >&2 + exit 1 +else + echo "reach: the package is not the recorded input; outputs are in $DIST/manifest.json only" +fi diff --git a/wai/wai-web/reach/manifest.json b/wai/wai-web/reach/manifest.json new file mode 100644 index 000000000..240a98e66 --- /dev/null +++ b/wai/wai-web/reach/manifest.json @@ -0,0 +1,140 @@ +{ + "format": "wai-reach-manifest/1", + "input": { + "files": { + "wai_web_bg.wasm": { + "sha256": "c2b63f747c8b55b3eb74fd9dd53109bb4031a578622e07c62e26d1a27eb2050e", + "bytes": 2586670 + }, + "wai_web.js": { + "sha256": "f63605f012b44b935af11ea7285b0ca57ef9a760158353b1d826304d54930f6a", + "bytes": 448554 + } + }, + "source": "the package of the WAI site's /wai/pkg/, kept in reach/record/: wai/wai-web at commit 248846f13f2da2db32914cff1e716064eaf3d039, built by wasm-pack 0.15.0 (web target, release, wasm-opt off) with rustc 1.98.0 on macOS arm64, the commit's wai/ exported to /private/tmp/wai-wasm-pkg/open-standards, and the export, the Cargo home, the toolchain and the standard library's sources remapped to /open-standards, /cargo, /rustup and /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea", + "commit": "248846f13f2da2db32914cff1e716064eaf3d039" + }, + "tools": { + "binaryen": { + "release": "version_123", + "archives_sha256": { + "arm64-macos": "74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b", + "x86_64-macos": "cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd", + "x86_64-linux": "e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe", + "aarch64-linux": "4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376" + } + }, + "package_lock_sha256": "59c8234c2e5dcc0e75a4e6270fb455be3a9281e800ca935432f4a03d2862ef8a", + "l1": "wasm-opt --mvp-features --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --legalize-js-interface", + "l2": "wasm-opt --mvp-features --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --signext-lowering --llvm-nontrapping-fptoint-lowering --llvm-memory-copy-fill-lowering, then wasm-opt --mvp-features --legalize-js-interface; reduced profiles: wasm-metadce with the profile's exports, wasm-opt -Oz -ocimfs=200, then legalised", + "j": "wasm2js -Oz -ocimfs=200 on the MVP-lowered module of each profile, run from binaryen's WebAssembly build of the same release (npm binaryen, pinned by package-lock.json)" + }, + "glue": { + "memory_view_checks": 10, + "symbol_dispose_guards": 62, + "bigint_sites_made_optional": 44, + "bigint_required_by": 2, + "typed_array_copies": 119 + }, + "outputs": { + "wai-load.js": { + "rung": "all", + "profile": null, + "what": "the loader", + "sha256": "c3dc79392e6bf75d6cc190cd38eabab94f449d0d3687585cae6c3e03fe6775a2", + "bytes": 16621, + "gzip": 5105, + "brotli": 4474 + }, + "wai_web.j.full.js": { + "rung": "J", + "profile": "full", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "1af5b5917ecb3097e8eb3c79342dd087bd3febeef58bd2222b623e0099b17848", + "bytes": 5969835, + "gzip": 1561026, + "brotli": 1053831 + }, + "wai_web.j.receipt.js": { + "rung": "J", + "profile": "receipt", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "b6c52b020c3daef6f0dc5a7ccbbd8e7134a951012987ceaaf66fdfaad9e2d77b", + "bytes": 996473, + "gzip": 309932, + "brotli": 251134 + }, + "wai_web.j.verify.js": { + "rung": "J", + "profile": "verify", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "4862fb576c72bb12bffe4799684450c81a3181a34db134118a44e12b9204ab76", + "bytes": 3086139, + "gzip": 844913, + "brotli": 606845 + }, + "wai_web.j.verify_decode.js": { + "rung": "J", + "profile": "verify_decode", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "d2bb946fbab8dbcc5fb7ca101b6c4615881d3f1f1de47813a0b3c9e080f1b1bf", + "bytes": 3310622, + "gzip": 903681, + "brotli": 642273 + }, + "wai_web.reach.js": { + "rung": "L1, L2", + "profile": "full", + "what": "runtime + i64 table + glue, for the wasm rungs", + "sha256": "747db637684c8ffed24aab175a3dc1a399901c1d9699dd99c86a090b4b78bf0c", + "bytes": 198994, + "gzip": 22998, + "brotli": 18502 + }, + "wai_web_bg.l1.wasm": { + "rung": "L1", + "profile": "full", + "what": "the shipped binary, legalised at the JS interface", + "sha256": "25e728714bcc67ccb133042012860a53c881ea6c26c3dc7538bf378c0598b3b8", + "bytes": 2591481, + "gzip": 918302, + "brotli": 683525 + }, + "wai_web_bg.l2.full.wasm": { + "rung": "L2", + "profile": "full", + "what": "MVP-lowered and legalised", + "sha256": "5af77d42fbd2e2e6023789aca42dcb62c6bcf1ccb29e7303b31de5e6251fa88e", + "bytes": 2591161, + "gzip": 919464, + "brotli": 684042 + }, + "wai_web_bg.l2.receipt.wasm": { + "rung": "L2", + "profile": "receipt", + "what": "MVP-lowered and legalised", + "sha256": "89ad094a90fbc31aa3399d0e4db41728c742c110a90480952f3c2527310fb821", + "bytes": 393027, + "gzip": 192859, + "brotli": 163989 + }, + "wai_web_bg.l2.verify.wasm": { + "rung": "L2", + "profile": "verify", + "what": "MVP-lowered and legalised", + "sha256": "cbf4d3705f566b76ea1aab6d4513a51632df0373338e0e206cceff9d4b7c6a00", + "bytes": 1222037, + "gzip": 521773, + "brotli": 407801 + }, + "wai_web_bg.l2.verify_decode.wasm": { + "rung": "L2", + "profile": "verify_decode", + "what": "MVP-lowered and legalised", + "sha256": "efe0033b1cfc151054baf6282bd6cd9b2a264cf5df8321586cdbcf1f62928b87", + "bytes": 1316304, + "gzip": 559206, + "brotli": 433878 + } + } +} diff --git a/wai/wai-web/reach/package-lock.json b/wai/wai-web/reach/package-lock.json new file mode 100644 index 000000000..b96ae8dab --- /dev/null +++ b/wai/wai-web/reach/package-lock.json @@ -0,0 +1,546 @@ +{ + "name": "wai-web-reach-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wai-web-reach-tools", + "license": "Apache-2.0", + "devDependencies": { + "acorn": "8.15.0", + "binaryen": "123.0.0", + "esbuild": "0.27.4", + "typescript": "5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/binaryen": { + "version": "123.0.0", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-123.0.0.tgz", + "integrity": "sha512-/hls/a309aZCc0itqP6uhoR+5DsKSlJVfB8Opd2BY9Ndghs84IScTunlyidyF4r2Xe3lQttnfBNIDjaNpj6mTw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-as": "bin/wasm-as", + "wasm-ctor-eval": "bin/wasm-ctor-eval", + "wasm-dis": "bin/wasm-dis", + "wasm-merge": "bin/wasm-merge", + "wasm-metadce": "bin/wasm-metadce", + "wasm-opt": "bin/wasm-opt", + "wasm-reduce": "bin/wasm-reduce", + "wasm-shell": "bin/wasm-shell", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/wai/wai-web/reach/package.json b/wai/wai-web/reach/package.json new file mode 100644 index 000000000..a28e5e00d --- /dev/null +++ b/wai/wai-web/reach/package.json @@ -0,0 +1,12 @@ +{ + "name": "wai-web-reach-tools", + "private": true, + "description": "Build-time tools of the WAI reach builds, pinned by package-lock.json. build.sh installs them with npm ci; nothing here is shipped. binaryen here is the release build.sh pins, compiled to WebAssembly: its wasm2js gives the same output on every host.", + "license": "Apache-2.0", + "devDependencies": { + "acorn": "8.15.0", + "binaryen": "123.0.0", + "esbuild": "0.27.4", + "typescript": "5.9.3" + } +} diff --git a/wai/wai-web/reach/profiles.json b/wai/wai-web/reach/profiles.json new file mode 100644 index 000000000..04ccc6ace --- /dev/null +++ b/wai/wai-web/reach/profiles.json @@ -0,0 +1,32 @@ +{ + "about": "The reduced profiles of the MVP-lowered builds (L2 and J): which exports of wai_web_bg.wasm each keeps. wasm-metadce keeps these and whatever they reach, and drops the rest. A prefix keeps every export that starts with it, and keeps the class's __wbg_free as well. 'always' is in every profile. The full profile keeps every export and is not listed. A page that calls a function outside its profile gets a TypeError from the glue.", + "always": { + "names": ["memory", "wasmMemoryBytes"], + "prefixes": ["__wbindgen_"] + }, + "profiles": { + "receipt": { + "about": "Envelope parse, asset provenance (content hash, Ed25519 signature, Merkle chain), and the hashes.", + "names": ["parse", "detectVersion", "sha256Hex", "blake3Hex", "fnv1aHex", + "waienvelope_capability", "waienvelope_kind", "waienvelope_media", "waienvelope_intent", + "waienvelope_fallback", "waienvelope_payload_len", "__wbg_waienvelope_free"], + "prefixes": ["assetprovenance_"] + }, + "verify": { + "about": "receipt, plus multi-rendition envelopes, pins, capability checks, stage identities, session claims and the output tree.", + "includes": ["receipt"], + "names": ["parseMulti", "stageContentHash", "stageLayerInfo", "verifyClaim", "verifyPin", "outputRoot", + "isCandidate", "isRegistered", "known_capabilities", "checkRefusal"], + "prefixes": ["waienvelope_", "waimulti_", "rendition_"] + }, + "verify_decode": { + "about": "verify, plus the integer decoders (WIV1, WIH1, WIA1, WIS1, int_synth, the JSON integer forms) and the staged-delivery sink.", + "includes": ["verify"], + "names": ["decodeWiv1", "decodeWih1", "decodeWia1", "decodeWis1", "decodeIntAudio", "decodeIntVideo", + "decodeIntVideoPframes", "decodeIntHyper", "decodeIntHyperBin", "decodeIntSynth", "decodeIntSplat", + "intPayloadCost"], + "prefixes": ["decodedaudio_", "decodedvideo_", "decodedimage_", "decodedsplat_", "intpayloadcost_", + "stagebase_", "stagesink_"] + } + } +} diff --git a/wai/wai-web/reach/record/wai_web.js b/wai/wai-web/reach/record/wai_web.js new file mode 100644 index 000000000..7f78f433c --- /dev/null +++ b/wai/wai-web/reach/record/wai_web.js @@ -0,0 +1,12364 @@ +/* @ts-self-types="./wai_web.d.ts" */ + +/** + * A signing actor in the browser. The page supplies 32 random bytes + * (crypto.getRandomValues); the key never leaves the tab — only + * signed ops do. An op is the unit of play: what travels between + * tabs (by any transport — transport is out of the extension's + * scope) is exactly what a native sink would ingest. + */ +export class Actor { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + ActorFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_actor_free(ptr, 0); + } + /** + * @param {Uint8Array} seed + */ + constructor(seed) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.actor_new(retptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + this.__wbg_ptr = r0; + ActorFinalization.register(this, this.__wbg_ptr, this); + return this; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Lamport discipline: an actor's counter MUST exceed every + * lamport it has observed. The page calls this with the lamports + * of ops it receives. + * @param {bigint} lamport + */ + observe_lamport(lamport) { + wasm.actor_observe_lamport(this.__wbg_ptr, lamport); + } + /** + * @returns {string} + */ + pubkey_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.actor_pubkey_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * Sign a truss IMPULSE op (the SUM-resolved shove) targeting + * `node_id` (16 bytes) at `effect_tick`. `dx/dy/dz` are authoring + * floats converted to Q32.32 *here, once* — the canonical artifact + * is the returned op bytes, so every sink derives identical state + * from them regardless of who authored or how. + * @param {bigint} effect_tick + * @param {Uint8Array} node_id + * @param {number} dx + * @param {number} dy + * @param {number} dz + * @returns {Uint8Array} + */ + sign_truss_impulse(effect_tick, node_id, dx, dy, dz) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(node_id, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.actor_sign_truss_impulse(retptr, this.__wbg_ptr, effect_tick, ptr0, len0, dx, dy, dz); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_export4(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +if (Symbol.dispose) Actor.prototype[Symbol.dispose] = Actor.prototype.free; + +export class AssetProvenance { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AssetProvenanceFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_assetprovenance_free(ptr, 0); + } + /** + * @returns {string} + */ + asset_hash_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_asset_hash_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * @returns {string} + */ + capability() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_capability(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * The hash this sink actually computed over the carried bytes (shown + * next to `asset_hash` so a mismatch is visible). + * @returns {string} + */ + computed_hash_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_computed_hash_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * The carried bytes' content hash equals the receipt's `asset_hash` + * — the asset is exactly what the receipt vouches for. A 1-bit change + * to the envelope flips this to false. + * @returns {boolean} + */ + content_ok() { + const ret = wasm.assetprovenance_content_ok(this.__wbg_ptr); + return ret !== 0; + } + /** + * @returns {string} + */ + media() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_media(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * Open an asset envelope + its provenance receipt JSON. Constructing + * with deliberately-mutated `envelope` bytes is how the page shows + * tamper-evidence: `content_ok` then returns false. + * @param {Uint8Array} envelope + * @param {string} receipt_json + */ + constructor(envelope, receipt_json) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(envelope, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(receipt_json, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + wasm.assetprovenance_new(retptr, ptr0, len0, ptr1, len1); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + this.__wbg_ptr = r0; + AssetProvenanceFinalization.register(this, this.__wbg_ptr, this); + return this; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * The carried payload bytes — the codec-specific bytes a native + * element (``, `