diff --git a/.github/wai-conformance/verifiers/quantum-receipts.sh b/.github/wai-conformance/verifiers/quantum-receipts.sh index 5c85c7fa9..2d1e6d179 100644 --- a/.github/wai-conformance/verifiers/quantum-receipts.sh +++ b/.github/wai-conformance/verifiers/quantum-receipts.sh @@ -1,6 +1,6 @@ # step: quantum job receipts · receipt_verify.py # order: 701 -# paths: wai/extensions/quantum-providers.md +# paths: wai/extensions/quantum-providers.md wai/extensions/quantum-wasmq.md # working-directory: . # independent: quantum-receipts media python3 -I quantum-conformance/tools/receipt_differential.py --second-only --corpus quantum-conformance/receipts # @@ -16,6 +16,6 @@ python3 -I wai/quantum-conformance/receipts/generate.py --check python3 -I wai/quantum-conformance/receipts/generate.py --self-test python3 -I wai/quantum-conformance/tools/receipt_verify.py --self-test wai/wai-quantum/src/engine/testdata/es-number-tostring.txt -for plant in lenient-key no-alg last-duplicate no-inexact measured-simulator whole-cap lax-small-order-r no-canonical-spelling set-seal-only names-layout-only; do +for plant in lenient-key no-alg last-duplicate no-inexact measured-simulator whole-cap lax-small-order-r no-canonical-spelling set-seal-only names-layout-only wasmq-unchecked; do python3 -I wai/quantum-conformance/tools/receipt_differential.py --second-only --plant "$plant" | tail -1 done diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml index 83dbfb69f..cfee39bcc 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -29,33 +29,44 @@ on: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/wai-wasmq/**' + - 'wai/quantum-conformance/eval/**' + - 'wai/quantum-conformance/wasmq/**' - 'wai/wai-quantum-writer/**' # The writer's model runs on this crate, which it path-depends on. - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-loader-gguf/**' - - 'wai/quantum-conformance/eval/**' - 'wai/quantum-cross-simulator/**' - 'wai/quantum-conformance/receipts/**' - 'wai/quantum-conformance/tools/receipt_verify.py' - 'wai/quantum-conformance/tools/receipt_differential.py' + - 'wai/quantum-conformance/tools/wasmq_mutants.py' - 'wai/quantum-conformance/tools/referee.py' - 'wai/extensions/quantum-providers.md' + - 'wai/extensions/quantum-wasmq.md' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' + # Nightly, beside the pushes: each rule's weakenings (below), which take a second build. + schedule: + - cron: '41 3 * * *' pull_request: paths: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/wai-wasmq/**' + - 'wai/quantum-conformance/eval/**' + - 'wai/quantum-conformance/wasmq/**' - 'wai/wai-quantum-writer/**' # The writer's model runs on this crate, which it path-depends on. - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-loader-gguf/**' - - 'wai/quantum-conformance/eval/**' - 'wai/quantum-cross-simulator/**' - 'wai/quantum-conformance/receipts/**' - 'wai/quantum-conformance/tools/receipt_verify.py' - 'wai/quantum-conformance/tools/receipt_differential.py' + - 'wai/quantum-conformance/tools/wasmq_mutants.py' - 'wai/quantum-conformance/tools/referee.py' - 'wai/extensions/quantum-providers.md' + - 'wai/extensions/quantum-wasmq.md' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' @@ -194,9 +205,27 @@ jobs: python3 -I wai/quantum-conformance/receipts/generate.py --self-test python3 -I wai/quantum-conformance/tools/receipt_verify.py --self-test wai/wai-quantum/src/engine/testdata/es-number-tostring.txt python3 -I wai/quantum-conformance/tools/receipt_differential.py --reference wai/wai-quantum-cli/target/release/wai-quantum - for plant in lenient-key no-alg last-duplicate no-inexact measured-simulator whole-cap lax-small-order-r no-canonical-spelling set-seal-only names-layout-only; do + for plant in lenient-key no-alg last-duplicate no-inexact measured-simulator whole-cap lax-small-order-r no-canonical-spelling set-seal-only names-layout-only wasmq-unchecked; do python3 -I wai/quantum-conformance/tools/receipt_differential.py --reference wai/wai-quantum-cli/target/release/wai-quantum --plant "$plant" | tail -1 done + # Each rule of a wasmQ job's form weakened in each verifier, from its code (the check + # dropped, a comparison moved by one, a count or a digest read loosely, a null member read + # as absent and an absent one as null), and every weakening caught by the differential: + # the second verifier's as files, the engine's as one build that holds every weakening, + # each switched on alone (wasmq_mutants.py). Counted per rule. Nightly. + - name: Each rule's weakenings caught, in both verifiers + if: github.event_name == 'schedule' + shell: bash + run: | + set -euo pipefail + python3 -I wai/quantum-conformance/tools/wasmq_mutants.py python --reference wai/wai-quantum-cli/target/release/wai-quantum --jobs 4 + m="$RUNNER_TEMP/wasmq-mutants" + rm -rf "$m" && mkdir -p "$m" + git worktree add --detach "$m/wt" HEAD + python3 -I wai/quantum-conformance/tools/wasmq_mutants.py rust-schemata "$m/wt/wai/wai-quantum/src/engine/wasmq.rs" "$m/mutants.json" + (cd "$m/wt/wai/wai-quantum-cli" && CARGO_TARGET_DIR="$m/target" cargo build --locked --release) + python3 -I wai/quantum-conformance/tools/wasmq_mutants.py rust --reference "$m/target/release/wai-quantum" --mutants "$m/mutants.json" --jobs 4 + git worktree remove --force "$m/wt" # The release binary, pinned by version and SHA-256, rather than a build from source. # Every WAI workflow that runs wasmtime pins this one version, and every run passes # -W nan-canonicalization=y -W relaxed-simd-deterministic=y: every NaN a float @@ -264,6 +293,24 @@ jobs: wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y "$w" estimate --demo-key --json --qubit us-e3 < "$RUNNER_TEMP/est.qasm" > "$RUNNER_TEMP/wasi-estimate.json" 2> /dev/null cmp "$RUNNER_TEMP/native-estimate.json" "$RUNNER_TEMP/wasi-estimate.json" || { echo "::error::an estimate differs under wasmtime"; exit 1; } echo "the WASI component's output equals the native build's" + # verify gives its verdict at its own bound under wasm32 as natively: a job of 384 MiB, + # one of 2^22 values, and one more of each (too_large). + - name: The WASI component judges a job at verify's bound + shell: bash + working-directory: wai/wai-quantum-cli + run: | + set -euo pipefail + w=target/wasm32-wasip2/release/wai-quantum.wasm + n=target/release/wai-quantum + d=$(mktemp -d "$RUNNER_TEMP/bound.XXXX") + python3 -I tests/verify_at_bound.py "$d" + for f in long longer many more; do + $n verify --json "$d/$f.json" > "$d/native-$f.txt" || true + wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir "$d" "$w" verify --json "$d/$f.json" > "$d/wasi-$f.txt" || true + test -s "$d/native-$f.txt" || { echo "::error::no verdict at verify's bound ($f)"; exit 1; } + cmp "$d/native-$f.txt" "$d/wasi-$f.txt" || { echo "::error::verify at its bound differs under wasmtime ($f)"; exit 1; } + done + rm -rf "$d" cli-macos: name: command line · macos diff --git a/.github/workflows/wai-quantum-wasmq.yml b/.github/workflows/wai-quantum-wasmq.yml new file mode 100644 index 000000000..7c1d3473f --- /dev/null +++ b/.github/workflows/wai-quantum-wasmq.yml @@ -0,0 +1,197 @@ +name: wai-quantum wasmQ + +# wasmQ (wai/extensions/quantum-wasmq.md): an OpenQASM 3 program and the WebAssembly its +# `extern`s call, run with the same result bytes and fuel on every host and sealed in a +# receipt (wai/wai-wasmq). Each leg runs the crate's tests on its host and checks the +# conformance vectors, which pin each run's whole job digest, so every leg passing is the +# claim that the bytes are the same on linux x86_64, macOS arm64 and wasm32. The linux leg +# also rebuilds the reference modules and requires the modules of record byte for byte. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-wasmq/**' + - 'wai/quantum-conformance/wasmq/**' + - 'wai/extensions/quantum-wasmq.*' + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-wasmq.yml' + pull_request: + paths: + - 'wai/wai-wasmq/**' + - 'wai/quantum-conformance/wasmq/**' + - 'wai/extensions/quantum-wasmq.*' + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-wasmq.yml' + +jobs: + relevant: + name: 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-quantum-wasmq.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches wasmQ: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + native: + name: profile, engines, reference modules, adversarial, vectors · ${{ matrix.os }} + needs: relevant + if: needs.relevant.outputs.run == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, macos] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.99.1 + 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.99.1 + rustup toolchain install 1.99.1 --profile minimal --component clippy + rustup default 1.99.1 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: The crate's tests, every engine of this host + shell: bash + working-directory: wai/wai-wasmq + run: | + set -euo pipefail + cargo test --locked --release --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/wasmq.log" + if grep -q 'FAILED' "$RUNNER_TEMP/wasmq.log"; then echo "::error::a test failed"; exit 1; fi + # Every test file of the crate ran, and none ran empty: a file nobody runs, or + # one whose tests are all compiled out, would otherwise pass unseen. + for f in tests/*.rs; do + t=$(basename "$f" .rs) + awk -v t="tests/$t.rs" '$1 == "Running" && $2 == t { seen = 1; next } seen && /^test result: ok\. [1-9][0-9]* passed; 0 failed/ { ok = 1; exit } seen && /^test result:/ { exit } END { exit ok ? 0 : 1 }' "$RUNNER_TEMP/wasmq.log" \ + || { echo "::error::tests/$t.rs did not run and pass with at least one test"; exit 1; } + done + grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/wasmq.log" || { echo "::error::no test ran"; exit 1; } + - name: The vectors, each engine, and their digest + shell: bash + working-directory: wai/wai-wasmq + run: | + set -euo pipefail + for e in native interpreter interpreter-prepared; do + cargo run --locked --release -q --bin wasmq_vectors -- verify ../quantum-conformance/wasmq "$e" | tail -1 + done + - name: Clippy, warnings denied + shell: bash + working-directory: wai/wai-wasmq + run: cargo clippy --locked --release --all-targets -- -D warnings + # wai-quantum holds a wasmQ job's form beside its receipts (engine::wasmq), and its + # engine builds without them; each feature set builds, without warnings. + - name: wai-quantum's engine with the receipts and without them + if: matrix.os == 'linux' + shell: bash + working-directory: wai/wai-quantum + env: + RUSTFLAGS: -D warnings + run: | + set -euo pipefail + cargo check --locked --no-default-features --features quantum_engine --lib + cargo check --locked --no-default-features --features quantum_engine,quantum_receipt --lib + + modules: + name: the reference modules rebuild to their modules of record · linux + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.99.1 with the wasm32-unknown-unknown target + 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.99.1 + rustup toolchain install 1.99.1 --profile minimal + rustup default 1.99.1 + rustup target add --toolchain 1.99.1 wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Build and compare + shell: bash + run: bash wai/wai-wasmq/modules/build.sh --check + - name: The sources the modules of record were built from + shell: bash + run: bash wai/wai-wasmq/modules/build.sh --sources + + wasip2: + name: interpreter and vectors under wasm32-wasip2 (wasmtime) + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.99.1 with the wasm32-wasip2 target + 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.99.1 + rustup toolchain install 1.99.1 --profile minimal + rustup default 1.99.1 + rustup target add --toolchain 1.99.1 wasm32-wasip2 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # The release binary, pinned by version and SHA-256, with both determinism settings on + # every run (scripts/check-wasmtime-pin.py). Here wasmtime is the host the interpreter + # runs in; the modules the interpreter runs are interpreted, not compiled by it. + - name: Install wasmtime 50.0.1 + shell: bash + run: | + set -euo pipefail + [ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; } + if ! command -v xz >/dev/null 2>&1; then + SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq xz-utils + fi + v=50.0.1 + f="wasmtime-v$v-x86_64-linux.tar.xz" + curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \ + "https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f" + echo "685466455dd42a14774a86c36c8f2e45cbb9bd6d92a850139315df60a75498a5 $RUNNER_TEMP/$f" | sha256sum -c - + tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH" + - name: The tests under wasm32, the interpreter the only engine + shell: bash + working-directory: wai/wai-wasmq + run: | + set -euo pipefail + W=$(cd .. && pwd) + CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir $W::$W" \ + cargo test --locked --release --target wasm32-wasip2 --no-default-features --no-fail-fast \ + --lib --tests 2>&1 | tee "$RUNNER_TEMP/wasip2.log" + if grep -q 'FAILED' "$RUNNER_TEMP/wasip2.log"; then echo "::error::a test failed under wasip2"; exit 1; fi + for f in tests/*.rs; do + t=$(basename "$f" .rs) + awk -v t="tests/$t.rs" '$1 == "Running" && $2 == t { seen = 1; next } seen && /^test result: ok\. [1-9][0-9]* passed; 0 failed/ { ok = 1; exit } seen && /^test result:/ { exit } END { exit ok ? 0 : 1 }' "$RUNNER_TEMP/wasip2.log" \ + || { echo "::error::tests/$t.rs did not run and pass under wasip2 with at least one test"; exit 1; } + done + - name: The vectors under wasm32 + shell: bash + run: | + set -euo pipefail + cargo build --locked --release --target wasm32-wasip2 --manifest-path wai/wai-wasmq/Cargo.toml --no-default-features --bin wasmq_vectors + wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir wai/quantum-conformance/wasmq::/v \ + wai/wai-wasmq/target/wasm32-wasip2/release/wasmq_vectors.wasm verify /v interpreter,interpreter-prepared | tail -1 diff --git a/scripts/check-comparative-prose.sh b/scripts/check-comparative-prose.sh index c6e7e6a0b..6e5851c68 100755 --- a/scripts/check-comparative-prose.sh +++ b/scripts/check-comparative-prose.sh @@ -34,6 +34,7 @@ PHRASES=( '[Nn]obody signs' '[Nn]ot one signs' '[Nn]one signs' + '\b[Nn]either\b[^.]* signs?\b' '[Bb]est[- ]in[- ]class' '\b[Bb]eats\b [A-Z]' '\b[Ss]uperior(ity)?\b' diff --git a/scripts/check-wasmtime-pin-selftest.sh b/scripts/check-wasmtime-pin-selftest.sh index 4c68863d7..6c4c59f5b 100755 --- a/scripts/check-wasmtime-pin-selftest.sh +++ b/scripts/check-wasmtime-pin-selftest.sh @@ -62,6 +62,13 @@ expect() { # expect [text the output must hold] } mk; expect pass "the pinned release, run every way with both settings, passes" +mk; mkdir -p "$tmp/wai/c"; printf '[features]\nwasmtime = ["dep:wasmtime"]\n[target.x.dependencies]\nwasmtime = { version = "=50.0.1", default-features = false }\n' > "$tmp/wai/c/Cargo.toml" + expect pass "a crate that pins the release exactly passes" +mk; printf '[dependencies]\nwasmtime = { version = "50", default-features = false }\n' > "$tmp/wai/c/Cargo.toml" + expect fail "a crate on a caret range of the release fails" 'a crate pins the release the workflows run' +mk; printf '[dependencies]\nwasmtime = "=49.0.2"\n' > "$tmp/wai/c/Cargo.toml" + expect fail "a crate on another release fails" 'depends on wasmtime =49.0.2' +rm -rf "$tmp/wai" mk; rep "v=50.0.1" "v=49.0.2" expect fail "another version" "downloads wasmtime 49.0.2" diff --git a/scripts/check-wasmtime-pin.py b/scripts/check-wasmtime-pin.py index 1550e0da2..afc1cdb91 100755 --- a/scripts/check-wasmtime-pin.py +++ b/scripts/check-wasmtime-pin.py @@ -21,7 +21,10 @@ requires that a module path, and every `CARGO_TARGET__RUNNER` that is wasmtime, passes `-W nan-canonicalization=y` and `-W relaxed-simd-deterministic=y`; 4. at least one download and one run were found, so a check that looked in the wrong - place fails rather than passing on nothing. + place fails rather than passing on nothing; + 5. every crate under wai/ that depends on wasmtime as a library pins it to that release + exactly (`version = "=PIN"`), so the engine a crate embeds is the one the workflows + run (wai-wasmq's native engine, extensions/quantum-wasmq.md §4.6). check-wasmtime-pin.py [--root DIR] @@ -51,6 +54,9 @@ INVOKE = re.compile( r"(?:^|[\s;&|(`/\"'=])wasmtime\s+(?:(?:run|serve|compile)\b|[^\s-]\S*\.(?:c?wasm|wat)\b)" ) RUNNER = re.compile(r"CARGO_TARGET_\w+_RUNNER\s*[:=]\s*[\"']?wasmtime\b") +# A Cargo dependency on the wasmtime library, and the version it asks for. +CRATE_DEP = re.compile(r"^\s*wasmtime\s*=") +CRATE_VERSION = re.compile(r"version\s*=\s*\"([^\"]+)\"|^\s*wasmtime\s*=\s*\"([^\"]+)\"") def logical_lines(text): @@ -130,13 +136,34 @@ def main(argv): d, r = check_file(os.path.join(wf, name), f".github/workflows/{name}", findings) downloads += d runs += r + crates = 0 + wai = os.path.join(root, "wai") + for dirpath, dirnames, filenames in os.walk(wai): + dirnames[:] = [d for d in dirnames if d not in ("target", "node_modules", ".git")] + if "Cargo.toml" not in filenames: + continue + path = os.path.join(dirpath, "Cargo.toml") + rel = os.path.relpath(path, root) + section = "" + for n, line in enumerate(open(path, encoding="utf-8").read().splitlines(), 1): + if line.strip().startswith("["): + section = line.strip() + continue + # Only a dependency table names a crate; `wasmtime = [...]` in [features] does not. + if "dependencies" not in section or not CRATE_DEP.match(line): + continue + crates += 1 + v = CRATE_VERSION.search(line) + ver = (v.group(1) or v.group(2)) if v else None + if ver != "=" + PIN: + findings.append(f"{rel}:{n}: depends on wasmtime {ver or '(no version)'}; a crate pins the release the workflows run, \"={PIN}\"") if downloads == 0 or runs == 0: findings.append(f"found {downloads} wasmtime download(s) and {runs} run(s) under {wf}: this check looked in the wrong place") for f in findings: print(f"::error::{f}") if findings: return 1 - print(f"check-wasmtime-pin: {downloads} downloads of wasmtime {PIN}, each checked against its digest; {runs} runs, each with both settings") + print(f"check-wasmtime-pin: {downloads} downloads of wasmtime {PIN}, each checked against its digest; {runs} runs, each with both settings; {crates} crate dependencies, each on ={PIN}") return 0 diff --git a/scripts/test-public-text-checks.sh b/scripts/test-public-text-checks.sh index 0d856bcf2..25ef8d4ec 100755 --- a/scripts/test-public-text-checks.sh +++ b/scripts/test-public-text-checks.sh @@ -108,6 +108,8 @@ printf 'Carriage is not a differentiator any more.\n' > "$tmp/differentiator.md" printf 'It is not a rate-competitive codec.\n' > "$tmp/rate-competitive.md" printf 'This is the moat \n' > "$tmp/prose-empty.md" printf 'WAI_MOAT_DIR \n' > "$tmp/prose-reasoned.md" +printf 'Neither, in what they publish, meters the callout or signs what ran.\n' > "$tmp/neither-signs.md" +printf 'Neither engine appears in a receipt.\n' > "$tmp/neither-clean.md" printf 'The decoder is integer all the way down.\n' > "$tmp/prose-clean.md" expect 1 "prose: superiority" bash "$prose" "$tmp/superiority.md" @@ -119,6 +121,8 @@ expect 1 "prose: unclaimed" bash "$prose" "$tmp/unclaimed.md" expect 1 "prose: fast win" bash "$prose" "$tmp/fast-win.md" expect 1 "prose: differentiator" bash "$prose" "$tmp/differentiator.md" expect 1 "prose: rate-competitive" bash "$prose" "$tmp/rate-competitive.md" +expect 1 "prose: neither ... signs" bash "$prose" "$tmp/neither-signs.md" +expect 0 "prose: neither, of our own two things" bash "$prose" "$tmp/neither-clean.md" expect 1 "prose: an opt-out with an empty reason is not honoured" bash "$prose" "$tmp/prose-empty.md" expect 0 "prose: an opt-out with a reason is honoured" bash "$prose" "$tmp/prose-reasoned.md" expect 0 "prose: a clean file is clean" bash "$prose" "$tmp/prose-clean.md" diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 7661c203f..fffe0ef97 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -322,6 +322,7 @@ and a role are fixed once registered. | `wai.quantum.circuit` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.quantum.circuit2` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.quantum.job` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | +| `wai.quantum.wasmq` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.route.record` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.semantic.clip` | payload | registered | image | standalone | royalty-free | behavioral | — | §5 Neural capabilities | | `wai.semantic.perco2` | payload | registered | image | standalone | non-commercial | behavioral | — | §5 Neural capabilities | @@ -446,6 +447,7 @@ and a role are fixed once registered. | `wai.quantum.landscape/sample` | a fabric kernel: the sampling of an energy-landscape job | | `wai.quantum.circuit/1` | a contract revision: the `ext` of a wai.quantum.circuit payload's contract (quantum-sim §2.1), a part of the capability's definition | | `wai.quantum.circuit2/1` | a contract revision: the `ext` of a wai.quantum.circuit2 payload's contract (quantum-sim §2.1), a part of the capability's definition | +| `wai.quantum.wasmq/1` | a contract revision: the `profile` of a wai.quantum.wasmq payload's contract and of its run's result (quantum-wasmq §2.2, §6.4, §10), a part of the capability's definition | | `wai.quantum.job-record/1` | a document with no schema file: the record a job service's store keeps a job in (wai-quantum-service) | | `wai.quantum.provider-job/1` | a document with no schema file: the record wai-quantum-qpu keeps of a job on a person's provider, written before the job is sent | | `wai.quantum.optimize-checkpoint/1` | a document with no schema file: an optimisation's checkpoint (wai-quantum's `engine::variational`) | @@ -453,6 +455,7 @@ and a role are fixed once registered. | `wai.quantum.docs-manifest/1` | a document with no schema file: the list of documents tools/quantum-docs checks | | `wai.quantum.write/1` | a document with no schema file: the transcript wai-quantum-writer keeps of writing a program for a task | | `wai.quantum.model/1` | a document with no schema file: the manifest of a writer's model, the file wai-model.json (wai-quantum-writer's `coder`) | +| `wai.quantum.wasmq.result/1` | a document with no schema file: the result of a wasmQ run, held to its form by wai-quantum's `engine::wasmq` (quantum-wasmq §6.4, §6.5) | | `wai.quantum.job-key/1` | a digest domain: the digest a job service keeps a client's idempotency key under, the client's name beside the key (wai-quantum-service) | | `wai.quantum.trace/1` | a digest domain: an optimisation trace's first link is BLAKE3 of it and the request's canonical text (wai-quantum's `engine::variational`) | | `wai.quantum.circuit3` | refused on purpose: not a capability; the circuit core's test gives it to see no gateset read from it | @@ -463,6 +466,9 @@ and a role are fixed once registered. | `wai.quantum.provider/2` | refused on purpose: a version of the provider protocol that no text defines; the reference provider's test sends it to see it answered unsupported_version (quantum-providers §10) | | `wai.quantum.provider/9` | refused on purpose: a version of the provider protocol that no text defines; the protocol schema's test sends it to see it answered unsupported_version (quantum-providers §10) | | `wai.quantum.provider/999` | refused on purpose: a version of the provider protocol that no text defines; the provider conformance corpus sends it to see it answered unsupported_version (quantum-providers §10) | +| `wai.quantum.wasmq/2` | refused on purpose: a wasmQ profile that no text defines; wasmQ's tests, the receipts corpus and its verifiers put it in a contract or a result to see the job refused (quantum-wasmq §6.5, rules 4 and 11) | +| `wai.quantum.wasmq/1x` | refused on purpose: the wasmQ profile one letter longer; the receipts' differential check puts it in a contract to see both verifiers refuse the job (quantum-wasmq §6.5, rule 4) | +| `wai.quantum.wasmx` | refused on purpose: not a capability; wasmQ's adversarial test and its vectors put it in a payload's manifest to see the envelope refused with W013 (quantum-wasmq §2.3) | | wire identifier (not a capability) | kind | status | defined in | |---|---|---|---| diff --git a/wai/SPEC.md b/wai/SPEC.md index 795e57c16..9e8c633ed 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -1008,6 +1008,7 @@ standard. See the linked extension for each. | `wai.ternary.tt` | WQTT container (balanced-ternary MPS cores — packed trits + integer per-column scales) | a **quantum-inspired** (tensor-network) transform ported from the ternary/MPS fabric with ONE change: the per-column scale is an **integer**, so the whole decode is `i64`/`i128` and the reconstruction is byte-identical on every machine — **reconstruction-equivalence** (a portable recon hash), receiptable. Scope: a **provenance** transform, measured at 13.3 dB SNR for 3.8× with six residual layers (`Lossy` tier; the extension's §5). Encoder-side float SVD only · [ternary-tt](extensions/ternary-tt.md) | | `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder transports the **QFT** ("quantum FFT") as a circuit, reconstructed byte-exact; the phase table is frozen as existing content was made with it, so `P(k)` deviates from the ideal phase for k ≥ 8 and applies none for k ≥ 19, and new content uses `wai.quantum.circuit2`. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | | `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (each component of every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.wasmq` | WAI1 envelope whose payload is a `WQM1` bundle: an OpenQASM 3 program of Clifford gates, the WebAssembly modules its `extern`s call and their priors, each pinned by BLAKE3 in a canonical contract | **result-equivalence** · [quantum-wasmq](extensions/quantum-wasmq.md) §6.4: every conforming runner gives an envelope the same result bytes, the same fuel for every callout and the same fault, on every host and engine. A run is whole-number arithmetic throughout: a stabilizer tableau, outcomes from an integer stream, and modules held to a deterministic profile (Lime1, nothing imported, memory and stack bounded, NaNs canonical, fuel counted on the module as written), sealed in the job receipt | *Scene carriage* ([scene-carriage](extensions/scene-carriage.md)) maps a world (`WWLD`), an audio scene (`WSCN`), the assets a world names and timed media items onto ISO/IEC diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md new file mode 100644 index 000000000..944239e5a --- /dev/null +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -0,0 +1,162 @@ +### wasmQ: an OpenQASM 3 program and the WebAssembly its `extern`s call, run the same everywhere, metered and receipted + +- **The extension** [`extensions/quantum-wasmq.md`](extensions/quantum-wasmq.md) registers + `wai.quantum.wasmq` (SPEC §5): a WAI1 envelope whose payload carries an OpenQASM 3 program, + the WebAssembly modules that define its `extern` functions and their priors, each pinned by + BLAKE3 in a canonical contract, so the envelope's digest names all of it. Its interface is + [`extensions/quantum-wasmq.wit`](extensions/quantum-wasmq.wit). +- **Whole numbers from end to end.** This version runs Clifford programs (the gates that take + no angle) on a stabilizer tableau, with each random outcome one bit of an integer stream the + text defines, so a result's bytes are the same on every conforming runner. +- **The profile.** A module is a core module in the Lime1 feature set that imports nothing, + bounds its memory and its size, and has no start function, threads, vectors, references or + growing table; each rule has a refusal code (`W001`–`W017`), and a reader refuses on reading + everything a run could not finish for a reason of the bundle's own. WebAssembly 3.0's + deterministic profile leaves `memory.grow`, `table.grow` and the call depth to the host; the + profile fixes all three. +- **What a run asks of its host.** A reader bounds from the bundle alone, and a program as it + reads it, what a run asks of a host outside its modules' fuel: the program's text, nesting, + qubits, bits, steps and the bits its calls pass, the memory and code among a bundle's modules, + an envelope's size, its results, and the work of its steps, its calls, its instances, its + compiling and the most fuel it may be given. So an envelope that opens on one host opens and + finishes on every host, a 32-bit one among them. The reader's nesting bound (`Q130`, as every + reading of a program counts it) and its bounds for programs from others (`Q323`) hold for a + bundle's program; `Q209` keeps its meaning, definitions and calls past 32. +- **A determined measurement, word by word.** The stabilizer tableau finds a determined + outcome as the sign of the stabilizers' product, a machine word of rows at a time, without + copying the tableau: at the most qubits, milliseconds where it took seconds, with the same + outcome, which a test holds to the full product on random states. +- **Fuel and stack, defined on the module.** Fuel is charged at the start of each run of + instructions, with the weights of the pinned wasmtime's own default count, and a page a + module grows at what zeroing it costs. The stack is + bounded by weight: each function's frame weighs a base and its parameters, results, locals, + the most operands its body holds and its bytes, and the frames suspended at calls may weigh 2^23 together, so the bound holds for + a frame of any size. A test holds the native engine to eight bytes of stack for each unit. +- **Two engines, one result.** The pinned wasmtime runs the module prepared to meter itself; + an interpreter of the crate's own, which also runs on wasm32, meters it itself. Every + instruction, charge, trap and fault agrees between them, and a host reaches a module's + memory the same way on both, as its export `memory`. +- **Callouts in the program.** The reader takes `extern` calls as steps when asked + (`qasm::read_callouts`, which returns the program with its steps, `qasm::Step`, and its + `extern`s): a call runs in program order between gates, its arguments bits or integers + known on reading, its result written to bits a later `if` reads. Every other reading still + refuses them (`Q125`), and nothing in the reader's existing types changes. An `extern` lowers + to an integer function when its widths fit 64 bits, and to lists otherwise: the Component + Model's canonical ABI. +- **The receipt.** A run goes shot by shot and seals its result in the job receipt the + engine already signs: counts, the fuel of every callout, the modules' digests, a digest of + every call's inputs and outputs, and the fault that stopped it (fuel, stack, a trap or a + result past its width), as a signed fault receipt. Every member of the result is defined by + the text. +- **The command line**: `wai-quantum pack-wasmq`, `check-wasmq`, `run ENVELOPE.wai` + (`--engine`, `--transcript`, `--replay`, each record written and read a call at a time), and + `verify JOB --bundle ENVELOPE.wai`, which also runs the envelope again and compares byte for + byte. A flag of a program's run given with an envelope, or an envelope's given with a + program, is refused rather than ignored. A bundle goes to no provider: the provider protocol + carries no call between gates, and the bring-your-own-quantum boundary is as it was. +- **One verdict on every surface.** A wasmQ job is checked on the one path every job is + (`engine::job::check`), so `wai-quantum verify` (with `--json` and `--lines`), the job + service's `POST /v1/verify`, both MCP servers, the Python package, the desktop app, a view + and a page give it the same verdict. Its text is read as the provider protocol's §8.2 reads + every job's, and §8.4 gains one reason, `wasmq_form`: the job breaks a rule of + quantum-wasmq §6.5, whose thirty-five numbered rules hold the contract, the result, the fuel, + the fault and the counts to the profile, by what is sealed alone, under whichever key. Beside + the verdict, `wasmq` states how the run ended, each rule it breaks, and where the host's + unsealed words differ from what is sealed, which change no verdict. The form lives in the + engine (`engine::wasmq`), and the runner takes its constants from there. +- **Receipts of record.** The receipts corpus holds six wasmQ jobs the command line sealed + (a run, one of a single instance, and faults in a call, of the run's budget, in an init and + in a prior) and forty-four crafted from them: forty each breaking the one rule of §6.5 it + names and no other (every rule but the first, which no receipt can break alone, as every + place a number may be has a rule of its own, and rule 27, which rule 32 holds too), two + breaking two, and two the same sealed content written otherwise or under another envelope. The second verifier, written in + Python from §6.5 without the reference, gives `wasmq_form` on its own, and the differential + holds the two to each other on every one-member change of each of the six; a planted + verifier that skips the rules (`wasmq-unchecked`) is caught. +- **Every surface, every body.** Text that does not read as a job's, a body that is no job, + and a result with no receipt are each a verdict with that reason alone on every surface: + the command line, both MCP tools, the job service over HTTP (a 200 verdict, no longer a + 400), the Python package, the desktop app, a page and a view. The service's tool given a + job's members checks the body it is given, its unsealed status among them. The Python + package judges a dict as the text it writes for it, and reads text that begins with a byte + order mark as text. A job routes to an estimate's or an algorithm's verifier only when it + is not a wasmQ job (`job::has_own_verifier`). +- **The command line's `--lines`** reads each line on its own: a line that is not UTF-8, + or blank, is that line's `unreadable`, and a file of no line is refused. `verify` says a + job found unreadable or with no receipt was not judged, and runs no envelope for it. +- **The form, tightened.** A call that stopped the run is one of the calls made: its + callout's fuel counts it, and its largest call is at least what it used. A whole number + past 2^53 − 1 is no count however a reader holds it. Rule 1's walk is linear in the text, + however long a member's name. A member is present whatever its value, `null` among them. +- **Writer and verifier agree.** Each budget is at most 2^39 when packed (`W017`), as every + verifier holds a contract to it. The program's qubits are defined for a program that names + physical qubits, and the bits' order for several registers; a vector pins both. A hybrid + reading's refusal for controls offers no method its run cannot use. +- **Every tie the writer keeps is a rule.** Rules 20 to 35 hold what the runner guarantees + between members: + - a returned call used 1 to the most one call used, and the stopped one the fault's fuel; + when the most is a returned call's, the others used 1 each at least; + - each set of instances passed every prior, a prior costing its bytes and 3 at least, and + called the `wasmq-seed` or `init` a lifecycle fault shows its module exports; the set a + lifecycle fault stopped passed the priors of the modules before its own; the stopped + call's fuel is on top of all of it; and each prior passed whole fits one lifecycle call's + limit; + - a call's fault is in its callout's module, a prior's in a module with one, a + `bad_result` in a call or a prior; + - a lifecycle fault is in shot 0 for one instance a run, and before any call in shot 0; + - a fuel fault used its whole limit, and any other fault at least what its reason shows + was charged (1; 2 for `bad_result`; 788 for `stack_exhausted`, the frames §3.2 allows + to pass 2^23); + - with no call returned, the transcript is the run's end alone, which the receipt holds; + - §4.8's products and sums over what a receipt shows: a shot's bits (a name's length in + characters), the shots' bits, the work (4097 for each call made among it), the calls a + shot's 2^21 steps hold, and the envelope's bytes, its manifest and its contract with its + frames. + + §6.5 says what it leaves free, and why, among it a program's and a module's least length + and an `extern`'s spelling; a result's `qubits` is at least 1. Rule 6 no longer repeats the bounds rules 6 and 31 imply, and rules 8, + 14 and 18 no longer repeat what rules 20, 25 and 30 hold. +- **The outcome, exactly.** §6.1 gives a shot's outcome as every bit the program declares at + its top level, written or not; a block's own bits are in no outcome; a measurement in a + program that calls externs names its bit (`Q324` refuses one that does not). Vectors pin + bits nothing writes and a block's own bit, and refuse a measurement with no bit. +- **The differential's rules stage**: each rule of the form at its bound, on both sides, + sealed again, each verdict written by hand, the review's labelled cases among them; the + second verifier's own hand-labelled cases run through the reference as well; and weakenings + of each rule, generated in both verifiers, each caught. +- **One answer, every kind of job.** An estimate or an algorithm's answer with no receipt, or + that is no job, is that reason alone on the command line, its MCP tool, `POST /v1/verify` + and the service's tool, as every other job is; one is run again only once its receipt and + §8.4's rules hold (`job::answer_of`, `job::answer_of_text`), and the service runs an + algorithm's answer again as the command line does, its tool alike given the job's text, + the job, or its members. A job past what a surface reads, in its bytes or in its values, is + `too_large`, a new reason of §8.4 reported alone, stating both (`read_at_most`): + `wai-quantum verify` reads 384 MiB and 2^22 values on every host, in about 1 GiB of memory; + `POST /v1/verify` and `/mcp` read 4 MiB and 2^19 values, and check off the service's async + workers, two at a time. `/mcp` answers a verify call past that with its verdict by its id, + reading a message past six times that only as far as its id, method and tool. A check + holds one reading of a job at a time. A job of any depth reaches its verdict through a + view's request and an MCP call, `unreadable` past 127, judged before the message is read + whole. An estimate whose request runs again nowhere is not valid and says why + (`rerun_error`), as an algorithm's answer does, on every surface. `verify` prints the verdict + of text that does not read and of no job. The Python package reads a str as a path only + when it names a file, and gives a verdict for every str and dict, a lone surrogate's + among them. +- **Breaking:** `POST /v1/verify` answers 200 with a verdict for a body that is not JSON, no + job, has no receipt (`unreadable`, `not_a_job`, `no_receipt`) or is past 4 MiB or 2^19 + values (`too_large`), where it answered 400 for the first three and 413 past 1 MiB; a + client reads `valid` and `reasons`. Its OpenAPI document says + so. `wai-quantum verify` reads at most 384 MiB and 2^22 values of a job, where it read any + file whole. +- **Breaking:** the command line's MCP server answers a line that is not UTF-8 with JSON-RPC + -32700 and reads on, where it ended its session. +- **Breaking:** the Python package's `verify` given a str that names no file reads it as the + job's text and gives its verdict (`unreadable` for a path that is not there), where it + raised `FileNotFoundError`. +- **Reference modules** from the engine's own code: the qLDPC decoder (matching for codes + with a matching graph, Relay-BP otherwise) and the atom rearrangement planner, modules of + record built on x86_64 Linux and committed with their sources' digests. Their results equal + the same functions compiled natively, called directly and in the middle of circuits. +- **Vectors** in `quantum-conformance/wasmq/`: envelopes with the result each must give on + every engine and host, and modules and envelopes a runner refuses, at least one for every + code, each one defect away from one that is accepted. diff --git a/wai/extensions/browser-sink.md b/wai/extensions/browser-sink.md index 673ee6223..92a6e7f5c 100644 --- a/wai/extensions/browser-sink.md +++ b/wai/extensions/browser-sink.md @@ -844,7 +844,7 @@ package of several components: the media classes `graphics` (7) and `text` (8) beside packaging's others, the rungs each medium takes, how each component is cut at the lead's piece starts, each piece's object and `target`, and the decoded-output digest each entry states. The reference package is written by -`wai-pack` under `compose/policy.toml`: the lead `v`, a 320 × 180 test pattern (the fixture +`wai-pack` under `compose/policy.toml`: the lead `v`, a test pattern of 320 × 180 (the fixture `compose-01`) with an integer base and a supplied AV1 floor for every piece; `g`, `vector-conformance`'s title card and credit roll; `t`, three captions; `s`, a score with a FLAC floor of each piece's mixdown, which this page does not decode (no route: it holds no diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index d6ac9e52b..98da0c92b 100644 --- a/wai/extensions/quantum-providers.md +++ b/wai/extensions/quantum-providers.md @@ -542,6 +542,7 @@ as a set: none for a valid receipt. It is valid exactly when there is none. | `unreadable` | the text breaks a rule of reading (§8.2). Reported alone | | `not_a_job` | the value is not an object whose `request` and `result` are both objects. Reported alone | | `no_receipt` | the result has no `receipt`. Reported alone | +| `too_large` | the job is longer than the surface reads, in its bytes or in its values (one, and one more for each `,`, `:`, `[` and `{` outside its strings: what a check holds follows them), which it states with the verdict (`read_at_most`: `{"bytes", "values"}`); it judged none of it. Reported alone. `wai-quantum verify`, on every host, reads 384 MiB and 2^22 values, more than any job holds; the job service's `POST /v1/verify` and `/mcp` read 4 MiB and 2^19 values. A job given as a string is its text's bytes and values; one given parsed in a message, as it is written there | | `receipt_form` | the receipt is not an object; its `alg` is not `ed25519`; it has a `canon` other than `rfc8785`; or its `key`, `digest` or `signature` is not lower-case hex of its length. When it is reported, the next three are not checked | | `digest_mismatch` | the digest the receipt states is not the digest (§8.2) of this request and result | | `bad_key` | `key` is refused by the strict rules (§8.2). When it is reported, the signature is not checked | @@ -556,6 +557,7 @@ as a set: none for a valid receipt. It is valid exactly when there is none. | `execution` | a stated provider's result whose `execution` breaks a rule of §8.3; or, for a kind with rows, is neither one of the rows that hold for its provenance nor stated by `nobody`; or, for `http` and `process`, is not stated by `provider` or `nobody` | | `compiled_program` | a stated provider's result whose `compiled.program` is not the BLAKE3 of `compiled.qasm3`'s UTF-8 bytes, when `qasm3` is a string and `program` is 64 lower-case hex digits | | `cost_confirmation` | a stated provider's result whose `cost.confirmed` is `true` on a free run or `false` on one that is not free (§7), when both are `true` or `false` | +| `wasmq_form` | a wasmQ job's result (its `format` is `wai.quantum.wasmq.result/1`, [quantum-wasmq](quantum-wasmq.md)) whose request or result breaks a rule of [quantum-wasmq §6.5](quantum-wasmq.md#65-verifying): the job is not one that profile writes | A provider's result is one whose `method` is `provider`. It is *stated* when it carries a member `counts_source` or a member `execution` (whatever their @@ -598,6 +600,12 @@ A verifier also states, for a provider's result (one with no receipt included), its class as its provenance reads it, and, in the stated form, its counts' source and its `execution`, each as the result gives it. +A wasmQ job's result is held to `wasmq_form` whatever its `method`, and to the provider +rules too when its `method` is `provider`. A verifier also states, for a wasmQ job, what +quantum-wasmq §6.5 says it states beside the verdict: how the run ended, each rule it +breaks and where the body's unsealed members differ from what is sealed. None of that +changes the verdict. + These are all the rules: what §8.1 names and this table does not check (`compiled.hash` and `placement`, the notes, the `ideal` figures, the estimate's contents) is the host's word, unchecked. The schemas @@ -686,6 +694,10 @@ whether each `qasm` is that program (`derived`, beside the reasons); that is the engine's check of the set, not a rule of its receipt, and a verifier that does not write programs out does not make it. +A program that calls WebAssembly between its gates ([quantum-wasmq](quantum-wasmq.md)) +is not a program of §4: it has steps between gates that this protocol does not carry. A +client MUST NOT send one to a provider; it runs where its runner is (quantum-wasmq §7). + ## 9. Conformance `wai-quantum-qpu/conformance/provider-v1.json` holds the vectors: @@ -731,9 +743,17 @@ surface that verifies is held to the manifest by its own tests, given each receipt's text: the job service over HTTP and as a tool, the command line's tool (each takes the job's text), the Python package (text, bytes or a path), the desktop app and a browser tab. A surface given a job already parsed into objects -(a tool's `request` and `result`, a Python dict) cannot see what parsing loses: +(a tool's `request` and `result`, a view's object) cannot see what parsing loses: a member named twice (`unreadable`), and a whole number past what its reader -keeps (`inexact_number`). It reports every other reason, and is held to the +keeps (`inexact_number`). A job parsed deeper than a text is read (127) is +`unreadable` there too, however deep it is: a message that carries a job (a tool's +call, a view's request) is read so that the job reaches its verdict (a job nested past +127, or past what the surface reads, is judged before the message is read whole, and the +message around it may nest at most 8 more), and a message that does not read is the +protocol's error, as a line that is not UTF-8 is to the command line's MCP server, which +answers it and reads on. The Python package judges a dict as the text it writes +for it, so a dict's nesting and its whole numbers are judged as text's are; only a +member named twice is past what a dict can hold. It reports every other reason, and is held to the manifest on every receipt but those. A tool's text is a JSON string, which holds UTF-8 only, so text that is not UTF-8 reaches a verifier only as a file's bytes. diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md new file mode 100644 index 000000000..c68eda5dc --- /dev/null +++ b/wai/extensions/quantum-wasmq.md @@ -0,0 +1,988 @@ +# WAI Extension: Hybrid Quantum-Classical Execution (`wai.quantum.wasmq`) + +Status: **Draft.** Reference implementation: `wai-wasmq` (the profile, `src/profile.rs`; +the prepared module, `src/meter.rs`; the interpreter, `src/interp.rs`; the engines, +`src/engine.rs`; the lowering, `src/abi.rs`; the envelope, `src/envelope.rs`; the run and +its receipt, `src/run.rs`), the OpenQASM reader's callouts (`wai-quantum`, +`qasm::read_callouts`), and the command line's `pack-wasmq`, `check-wasmq`, `run` and +`verify` (`wai-quantum-cli`, `src/wasmq.rs`). A job's form (§6.5) is the engine's, where +every verifier of its receipts reaches it (`wai-quantum`, `engine::wasmq`). Interface: [`quantum-wasmq.wit`](quantum-wasmq.wit). +Vectors: [`../quantum-conformance/wasmq/`](../quantum-conformance/wasmq/). + +Keywords **MUST**, **MUST NOT**, **SHOULD** and **MAY** are RFC 2119/8174. + +## 0. What this extension adds + +A hybrid quantum program runs classical code between its gates: it decodes a syndrome +and corrects the error before the next round, or plans the moves that fill an atom +array. OpenQASM 3 declares such a function with `extern` and leaves its body, its binary +form and how it is linked outside the language. This extension fills that gap for WAI +Quantum, and nothing else: + +- **One envelope.** A WAI1 envelope of capability `wai.quantum.wasmq` carries the + OpenQASM 3 program, the WebAssembly modules that define its `extern`s, and the modules' + parameter files (priors), each pinned by BLAKE3 in a canonical contract (§2). +- **One profile.** A module is a core WebAssembly module in the Lime1 feature set, which + imports nothing, whose memory, size and stack are bounded, and which uses nothing whose + result a host may choose (§3). +- **The same bytes and the same fuel everywhere.** Fuel, the stack's weight and the NaN a + float instruction makes are defined on the module as written (§4), so every runner + gives a callout the same results, the same fuel and the same fault. The reference runs + each module on two engines, a native one and an interpreter that also runs on wasm32, + and every vector holds on both, on every host. +- **A receipt.** A run seals its result in the job receipt the engine already signs + (`engine::receipt`): the counts, the fuel each callout used, the modules' digests, a + digest of every call's inputs and outputs, and the fault that stopped the run, if one + did (§6). + +This version of the profile runs Clifford programs, in whole numbers from end to end +(§5.1, §6.1). A run simulates the quantum program and runs the classical code for real. +§7 says what holds when the quantum part runs on hardware, and §10 what this version +leaves out. + +## 1. Principles + +1. **Nothing outside reaches the result.** A module imports nothing: no clock, no + random source, no file, no network, no host function. Its only inputs are its bytes, + its priors, the seed the run gives it and the bits the program passes (§3.2, §5.3). +2. **The host chooses nothing.** Every place where WebAssembly lets an engine choose (a + NaN's bits, a relaxed vector instruction, how deep calls may go, when memory runs out, + when to stop a long computation) is fixed by the profile or refused (§3, §4). +3. **Fuel is the measure, not time.** A callout's budget is a number of weighted + instructions (§4.2). It is the same on every host; wall-clock time is not, and the + profile does not claim one. +4. **The direction is the hardware's.** The quantum program calls the module, as a + real-time controller calls classical code between gates. A module drives no gates. +5. **One form for one bundle.** A bundle has exactly one envelope, so its BLAKE3 names + it, and a receipt that names the digest names the program, the modules, the priors and + the run (§2.4). +6. **What is read is what runs.** A reader refuses on reading every bundle a run could + not finish for a reason of the bundle's own (§2.3). A run that starts ends in a + receipt, unless the host itself fails (§6.3). + +## 2. The envelope + +### 2.1 Container and manifest + +The envelope is a WAI1 container (SPEC §2). Its manifest is, in this order and with no +other member: + +```json +{"wai":"1.0","media":"binary","intent":"replicate", + "model_requirement":{"capability":"wai.quantum.wasmq","fallback":null}, + "conditioning":{"kind":"wasmq"}, + "target":{"qubits":17,"shots":32,"seed":1234}} +``` + +`fallback` is `null`: a sink without this capability holds no way to run the program's +callouts, so the envelope is inert there (SPEC §4). `target` is informational (SPEC §3) +and is the contract's: the program's qubits (§2.2), the shots and the seed. +`conditioning` carries no `prior`: SPEC §3.1's pin names a parameter set the sink +supplies, and a wasmQ envelope carries its modules' priors itself, each pinned in the +contract (§2.2). + +### 2.2 Payload and contract + +```text +payload = "WQM1" | u32 contract_len | contract | frames +frames = program | for each module, in order: the module, then each of its priors in order +``` + +`contract_len` is little-endian. The contract is JSON in the canonical form of RFC 8785 +(JCS), with exactly these members: + +| member | | +|---|---| +| `profile` | `"wai.quantum.wasmq/1"` | +| `program` | `{ "blake3", "bytes", "inputs" }`: the OpenQASM 3 source's digest and length, and the values of its integer `input`s (an object, empty when there are none) | +| `modules` | for each module: `{ "name", "blake3", "bytes", "priors": [{ "name", "blake3", "bytes" }] }` | +| `callouts` | one for each `extern` the program declares, in the order it declares them: `{ "extern", "module", "export" }`, the module by its index | +| `run` | `{ "shots", "seed", "method", "state", "fuel": { "per_call", "per_job", "per_instance" } }` (§6.1, §4.2) | + +`blake3` is the BLAKE3 digest of the frame as 64 lowercase hexadecimal digits; `bytes` +its length. A `name` is for people. `method` is `stabilizer`, the one method of this +version (§5.1). `state` is `shot` (a fresh instance of each module every shot) or `job` +(one instance for the run, its shots in order). `shots` is 1 to 2^20. `seed` and the +size of each input are at most 2^53 − 1, so that every JSON reader holds them exactly, and +each budget at most 2^39 (§4.8). `per_job` is held to less with any module: its eight +operations for each unit of fuel take all of a run's work at 2^39, and a run's other work +is never nothing (§4.8). Every module is named by +at least one callout. The program's text is at most 1 MiB, and the modules' +bytes at most 512 KiB among them. + +**The program's qubits** are the qubits it declares, all its registers together; a program +that declares none and names physical qubits (`$0`, `$5`) has one more than the highest +index it names, the qubits below it among them. + +### 2.3 Reading an envelope + +A reader MUST refuse an envelope, with the code of §3.3, when: + +- it is longer than 128 MiB, its first four bytes are not `WAI1`, its capability is not + `wai.quantum.wasmq`, its manifest is not the one §2.1 gives for its contract byte for + byte, or any byte follows the payload (`W013`); +- the payload's magic is not `WQM1`, the contract is not in its RFC 8785 form, holds a + member §2.2 does not list or lacks one, or the frames do not end exactly where the + payload ends (`W013`); +- a frame's BLAKE3 is not the one the contract pins: the program, a module, a prior + (`W014`); +- a module breaks the profile (§3: its code), the modules hold more than 4096 pages among + them (`W003`, §4.7), or more than 512 KiB of bytes (`W010`); +- the program is longer than 1 MiB, is refused by the reader, calls nothing, has a gate + outside §5.1's, or is past a bound §4.8 reads it within (`W016`); +- the callouts are not one for each `extern` in the order declared, a module is named + by none, a callout's export is not the type its `extern` lowers to, or a module the + host must write to or read a list from does not export what §5.2 asks (`W015`); +- the run is outside §2.2's ranges, or past what §4.8 lets a run ask of its host (`W017`). + +A writer checks the same rules and writes nothing that breaks one. An envelope that +passes them runs: nothing about a bundle is left for a run to find (§6.3). + +### 2.4 One envelope for one bundle + +A writer MUST write the frames in §2.2's order, the contract in its canonical form with +its callouts in the order the program declares its `extern`s, and the manifest exactly +as §2.1 gives it. A reader that holds these rules reads every bundle from exactly one +sequence of bytes, so the envelope's BLAKE3 (`envelope_blake3`) names the bundle, and a +receipt that binds it binds every frame. + +## 3. The module profile + +### 3.1 A core module + +A module is a core WebAssembly module (WebAssembly 3.0, §5 Binary Format), not a +component. It MAY be the core module of a component: its exports are the canonical +lowering of the functions [`quantum-wasmq.wit`](quantum-wasmq.wit) declares, so a +component host can wrap it, and a host of this profile calls it directly. + +### 3.2 The rules + +A module MUST validate under the **Lime1** feature set of the WebAssembly tool +conventions: WebAssembly 1.0 with mutable globals, multi-value, sign extension, +non-trapping float-to-int conversion, `memory.copy` and `memory.fill`, extended constant +expressions and the overlong `call_indirect` encoding. Every instruction is in the +encoding that feature set gives it, and every function body ends with the `end` of its +own block. And: + +| rule | why | +|---|---| +| It imports nothing. | Nothing outside the module reaches its result. A clock, a random source or any host function would. | +| It has at most one memory, 32-bit, unshared, of 64 KiB pages, with a maximum of at most 4096 pages (256 MiB). | A memory without a maximum grows as far as a host lets it, which differs by host. | +| It has no start function. | Nothing runs when an instance is made: every instruction runs in a metered call (§5.3). | +| Nothing shared, no atomic instruction. | Lime1 has no threads. | +| No vector instruction or type, relaxed or not. | A relaxed vector instruction's result is the engine's choice; the profile takes none. | +| No instruction or type of another feature outside Lime1: references, exceptions, tail calls, garbage collection, passive segments. | Lime1's values are numbers. | +| At most one table, of `funcref`, at most 2^16 entries, filled once from active segments of function indices; no instruction reads or changes it but `call_indirect`. | `table.grow` stays non-deterministic in the deterministic profile (below). | +| In every function, at most 4096 parameters, locals and operands at once. | It bounds what one frame holds in an interpreter. | +| At most 512 KiB of bytes, which is also what the modules of a bundle hold among them; at most 2^16 types, functions, globals, data segments and element segments each; at most 2^10 exports, each name at most 256 bytes; a function type of at most 2^8 parameters and 2^8 results; a function's entry in the code section of at most 2^14 bytes. | They bound what a reader, an engine and a frame need (§4.3), with room for what preparing a module adds (§4.4): a body of calls grows some 53 times prepared, and an engine compiles a function whole. | +| Every active data and element segment fits where it is placed. | Instantiation then cannot fail. | +| No export begins `__wasmq`, and an export a host calls has exactly its type (§5.2, §5.3). | The first are the prepared module's names (§4.4); a host passes the second arguments of those types. | + +WebAssembly 3.0 defines a deterministic profile: every NaN an instruction makes is +canonical and positive, and every relaxed vector instruction has a fixed behaviour. Even +under it, `memory.grow` and `table.grow` remain non-deterministic, to indicate resource +exhaustion, and the depth of calls an engine allows is an implementation limit. This +profile takes the deterministic profile's rule for NaNs (§4.5) and closes the others: no +vector instruction is taken, the table never grows, the memory grows only within the +maximum the module declares (§4.7), and the stack is weighed (§4.3). + +The limits of the last rows are each below the limits of the validator and the native +engine the reference uses, and stay below them once a module is prepared: a test makes a +module of the most bytes, its largest body all calls, with the most exports, names, table +entries and memory and as many of each other item as fit beside them, and prepares, +compiles and runs it on every engine (`wai-wasmq/tests/engines.rs`). + +### 3.3 Refusal codes + +| code | the module or envelope | +|---|---| +| `W001` | is not a valid core module in Lime1, for a reason no other code names (a component, a malformed module, a body that does not end, an encoding of another feature) | +| `W002` | imports something | +| `W003` | has more than one memory, a 64-bit one, one with a page size of its own, one without a maximum or past 4096 pages; or (envelope) has modules with more than 4096 pages among them | +| `W004` | has a start function | +| `W005` | has a shared memory, global, table or type, or an atomic instruction | +| `W006` | has a vector instruction or type, relaxed or not | +| `W007` | has an instruction or a type of a feature outside Lime1 that no other code names | +| `W008` | has more than one table, one not of `funcref`, past 2^16 entries, or an instruction that reads or changes it other than `call_indirect` | +| `W009` | has a function whose parameters, locals and operands can pass 4096 | +| `W010` | is past a size the last rows of §3.2 give; or (envelope) has modules of more than 512 KiB among them | +| `W011` | has a data or element segment that does not fit where it is placed | +| `W012` | exports a name the profile reserves, or an export a host calls with another type than its own | +| `W013` | (envelope) is not one this profile writes, or is past its size (§2.3) | +| `W014` | (envelope) has a frame whose digest is not the one its contract pins | +| `W015` | (envelope) has callouts that are not one for each `extern` in order, a module no callout names, an export of another type than its `extern` lowers to, or a module the host cannot reach as §5.2 asks | +| `W016` | (envelope) has a program longer than 1 MiB, that the reader refuses, that calls nothing, with a gate outside §5.1's, or past a bound it is read within (§4.8) | +| `W017` | (envelope) has a run outside the profile's ranges, or past what §4.8 lets a run ask of its host | + +A module or an envelope that breaks more than one rule is refused with the code of one +of them. Each refusal vector (§9) breaks exactly one. +`wai-quantum check-wasmq MODULE.wasm` holds a module to the profile and names the code. + +## 4. Running a module the same everywhere + +### 4.1 Instances + +An instance is made from the module with no imports. Its memory starts at its initial +size, zeroed, with the active data segments copied in; its table holds the active element +segments; its globals hold their initial values. A host MUST be able to hold the memory's +maximum before it calls the instance (§4.7). + +### 4.2 Fuel + +Every call a host makes into an instance has a limit of fuel. Fuel is defined on the +module as written, not on any engine: + +- A function's body is a sequence of **runs**. A run starts at the function's first + instruction and right after each `block`, `loop`, `if`, `else`, `end`, `br`, `br_if`, + `br_table`, `return`, `call`, `call_indirect` and `unreachable`; it ends with the first + of them that follows, or with the body. +- The **weight** of an instruction is 0 for `nop`, `drop`, `block`, `loop`, + `unreachable`, `return`, `else` and `end`, and 1 for every other. Entering a function + weighs 1. +- When execution reaches the start of a run, the run's weight (with 1 more for the first + run of a body) is charged. Before `memory.copy` and `memory.fill` execute, the byte + count they take is charged; before `memory.grow` executes, 1024 for each page it asks + for is charged, whether or not the growth succeeds: zeroing a page costs a host what + 1024 instructions do. +- When a charge takes the fuel below zero, the call stops with `fuel_exhausted`. + +So a charge happens at the start of a run, before any of the run's instructions, and a +call that traps in the middle of a run has been charged the whole run. On every path that +does not stop early, the fuel a call uses is the number of instructions it executed under +these weights, and one for each function it entered. + +**Limits.** The contract gives three budgets. A callout's limit is `per_call`, and it +covers every call the host makes for it (§5.2). The limit of an instance's lifecycle +calls is `per_instance`, for each of them (§5.3). `per_job` is the run's: when less than +a call's limit is left of it, what is left is the call's limit. The fuel a call used is +its limit less what it left, and is taken from what is left of the run's; a call that +stops with `fuel_exhausted` used its whole limit. + +The weights are the default operator costs of the native engine the reference uses, with +one for a byte of `memory.copy` or `memory.fill` and 1024 for a page of `memory.grow`. A +test holds the profile's fuel equal to that engine's own count, on the module as written, +over control flow, calls and memory instructions, with one difference it states: for +`memory.grow` the profile charges 1024 for each page asked for, and that engine's own +meter one for each page it gets (`wai-wasmq/tests/engines.rs`). + +Fuel is a count of work, the same on every host. It is not a time: a host that must +answer within a deadline measures its own instructions per second and sets the budget. + +### 4.3 The stack + +Calls are bounded by weight, not by count, so that the bound holds for a frame of any +size on any engine. The **weight of a function's frame** is + +```text +576 + its parameters + its results + its locals + its operands + the bytes of its entry in the code section +``` + +its parameters and results those of its type, its locals the ones it declares, its +operands the most values its body holds on the operand stack at once (as a validator +counts them after each instruction: a call's results among them), and its entry's bytes +the value of the entry's own size field. The **stack's weight** is zero +when a host calls an export. Before a `call` or `call_indirect` executes, the weight of +the function that holds the instruction is added to it; when the callee returns, the +same weight is taken off. A call that would take the stack's weight past **8 388 608** +(2^23) stops with `stack_exhausted`, before the instruction does anything else: a +`call_indirect` that is both past the stack and through an entry it cannot call stops +for the stack. + +So the frames suspended at calls never weigh more than 2^23 together, and one more +frame runs on top of them. An interpreter that keeps a frame's parameters, locals and +operands in slots needs at most one slot for each unit. The reference's native engine +needs at most eight bytes for each unit: a test runs modules made to keep as much in a +frame as the largest body allows (locals held across a call, a new value held at every +step, the results of calls held on the operand stack across a call, the largest function +types), each recursing to the limit, on a stack of exactly that size, and shows a smaller +one fails (`wai-wasmq/tests/engines.rs`). It gives each call nearly twice that. + +A function of a few instructions recurses some 14 000 deep; a function at the largest +body the profile takes, some 490. + +### 4.4 The prepared module + +A host MAY run a module with an engine that implements §4.2, §4.3 and §4.5 itself (the +reference interpreter does). The reference's native engine does not, so it runs the +**prepared module**: the module with every instruction kept byte for byte, and added + +1. two mutable globals, after the module's own, exported as `__wasmq_fuel` (i64) and + `__wasmq_stack` (i32); +2. at the start of each run whose weight is not zero, the charge + `global.get F; i64.const w; i64.sub; global.set F; global.get F; i64.const 0; i64.lt_s; if; unreachable; end`; +3. before `memory.copy` and `memory.fill`, the same charge of the byte count, and before + `memory.grow`, of 1024 times the page count, each taken from the operand with + `local.tee` of one added `i32` local; +4. around each `call` and `call_indirect`, with `w` the weight of the function they are + in, `global.get S; i32.const w; i32.add; global.set S; global.get S; i32.const 8388608; i32.gt_u; if; unreachable; end` + before it and `global.get S; i32.const w; i32.sub; global.set S` after it; +5. after each instruction of §4.5, a call of an added function that returns its argument, + or the canonical NaN if the argument is a NaN; + +with every custom section dropped. The host sets `__wasmq_fuel` to the call's limit and +`__wasmq_stack` to 0 before each call. When the prepared module traps at `unreachable` +with `__wasmq_fuel` below zero, the fault is `fuel_exhausted`; with `__wasmq_stack` past +2^23, `stack_exhausted`; otherwise the module's own `unreachable`. + +The prepared module is one way to hold §4.2, §4.3 and §4.5, and its bytes are the +reference's own: this text does not fix them, and no receipt names them. What a +conforming runner must reproduce is what those sections define. + +### 4.5 NaNs + +The result of `f32` and `f64` `add`, `sub`, `mul`, `div`, `min`, `max`, `sqrt`, `ceil`, +`floor`, `trunc` and `nearest`, of `f32.demote_f64` and of `f64.promote_f32` is, when it +is a NaN, the canonical positive NaN (`0x7fc00000`, `0x7ff8000000000000`): WebAssembly +3.0's deterministic profile. Every other instruction already has one result everywhere. +A test shows that a module whose result depends on a NaN's bits reads what the host chose +when neither the preparation nor the engine canonicalises, and the canonical NaN when +either does (`wai-wasmq/tests/adversarial.rs`). + +### 4.6 Engines + +The reference runs every module on two engines and holds them to the same bytes: + +- **native**: the repository's one pinned release of wasmtime, compiling the prepared + module with Cranelift, with NaN canonicalisation on, relaxed vector instructions fixed + and refused, vector instructions, tail calls and every proposal past Lime1 off, its + own fuel on as a backstop under the profile's, and a run's calls made on one thread with + a stack of the size §4.3 bounds; +- **interpreter**: `wai-wasmq`'s own, which runs the module as written and implements + §4.2, §4.3 and §4.5 itself, and which also runs on wasm32 (where the native engine + does not run). It also runs the prepared module, with its own metering off, as a check + of the preparation on the vectors; that use is held to none of §4.8's bounds. + +`wai-quantum run ENVELOPE.wai --engine native|interpreter` chooses; the receipt is the +same. + +### 4.7 Memory + +`memory.grow` past the module's declared maximum returns −1, on every engine. Within the +maximum, it succeeds: a host that cannot grow the memory within the maximum (it has run +out of memory) MUST stop the run and seal nothing (§6.3), because what it would seal +would be its own condition, not the module's. + +The modules of a bundle hold at most 4096 pages among them, by the maxima they declare +(`W003`), so that a 32-bit host holds any bundle's memory: a limit for each module alone +would leave the number of modules to decide which hosts can run an envelope. + +### 4.8 What a run asks of its host + +Fuel is the work of the modules. A run also asks work and memory of its host that no +fuel counts: reading the program, its steps on the tableau, a fresh instance's memory, +compiling its modules, the result. So that an envelope that opens on one host opens and +finishes on every host, a 32-bit one among them, a reader bounds all of it from the +bundle alone, before anything runs, and refuses past the bounds; nothing here differs by +engine. A receipt shows part of what these bounds count, and §6.5 holds a job to that part +(rules 29 to 32). + +**Reading.** The program is read within these bounds, each checked as it is read, so that +a short program cannot make a reader hold or do more than a host has before it is refused +(`W016`): + +| bound | | +|---|---| +| its text | 1 MiB (a reader holds some eighty bytes of tokens for each) | +| nesting | 128 (`Q130`): a reader's stack. A statement at the top level is at depth 1; each statement in a block or a body, each expression a statement holds, each operand of an operator, what a bracket holds and each index's indexed value is one deeper than what holds it; operators group as they bind, so a chain of 127 is at the bound; an `else if` is at its `if`'s depth, at most 1024 to one `if`; and a call's body is read in its place, one deeper than the call | +| qubits | 32 768 | +| bits: the program's and its blocks' own together, any one value's, and those of any one array, its elements times each element's | 2^20 | +| steps, its loops unrolled | 2^21 | +| the bits its calls take and give, summed over its steps | 2^23 | +| the work of reading: one for each statement and loop iteration, for each value worked out one and one more for each bit or element it makes, and one for each bit declared | 2^26 | + +Past one, the reader's code is `Q323`, or `Q111` for the qubits. **A run ends**: the +reader unrolls every loop, a `while` included, to the bounds it reads or is told +(`@wai.max_iterations`), so every jump in a program's steps goes forward. + +**Memory.** A run's shots times the bits a shot holds (all the program's bits, its blocks' +own among them, though an outcome shows only the program's, §6.1) are at most 2^28 +(`W017`). With the most memory (§4.7), the largest +envelope (§2.3), the most code (§3.2), the most qubits, bits and steps and the bits of +calls, these leave a run within a 32-bit host's address space at every limit at once, +which a test holds the reference to under wasm32 (`wai-wasmq/tests/sweep.rs`). + +**Work.** A run's work is at most 2^42 operations on a word (`W017`), where, with `n` the +program's qubits and `r` = ⌈`n`/32⌉ (at least 1) the words of a tableau's run for one +qubit: + +| for each shot | operations | +|---|---| +| its fresh tableau | 2·`n`·`r` | +| each gate | `r` | +| each measurement or reset | 2·`n`·`r` | +| each callout | 4096 for each call of the module's (its export, `cabi_realloc` for each list argument, and `cabi_post_` after a list result), 4 for each bit it takes and gives, and 1 | +| each other step | 1 | + +| for each instance, one for each shot, or one for the run when `state` is `job` | operations | +|---|---| +| each module, and each call of `wasmq-seed`, `init`, and two for each prior | 4096 | +| each byte of each module | 1 | +| each page of the memory a module starts with | 8192 | + +| for the run | operations | +|---|---| +| each byte of each module, compiled | 131 072 | +| each unit of the most fuel it may be given, `per_job` | 8 | + +A random measurement rewrites the tableau's rows that hold an X at its qubit; a determined +one's outcome is the sign of the product of up to `n` stabilizers, which the reference +finds a machine word of rows at a time without copying the tableau. Either reads the +tableau about once, which is the `n`·`r` of its weight. A page a module grows is paid for in fuel (§4.2), as +a page it starts with is counted here. This is a bound on a host's effort from the bundle +alone, not a measure of it: fuel is the profile's measure of the modules' work (§4.2). +The reference was measured at a 64th of the limit, each kind of work alone, on x86_64 +Linux natively and under wasm32: at the limit it would take at most about three hours. + +## 5. Callouts + +### 5.1 In the program + +A program declares each function it calls with `extern`, its arguments and its result +written as types: + +```qasm +extern decode64(bit[4]) -> bit[9]; +bit[4] s; +bit[9] corr; +s = measure a; +corr = decode64(s); +if (corr[0]) x d[0]; +``` + +An argument is `bit[n]`, `uint[k]` or `int[k]` (k from 1 to 64); the result is `bit[n]` +or nothing. An argument is a bit register or an integer known on reading (a constant, a +loop's variable, an `input`); the result goes into a bit register, in a declaration or +an assignment, and a later condition reads it. Calls inside `for` loops unroll; calls +under `if` run only on the shots that take the branch. + +**The gates.** This version runs the Clifford gates that take no angle: `id`, `x`, `y`, +`z`, `h`, `s`, `sdg`, `sx`, `sxdg`, `cx`, `cy`, `cz` and `swap`, and gates a program +defines from them, with measurement, reset and conditions on bits, on at most 32 768 +qubits. A program with any other gate is refused (`W016`), whatever its angle: `rz(pi/2)` +acts as `s`, and is refused all the same, so that no reader decides in floating point +which gate a program holds. With these gates a state is a stabilizer tableau and every +step of a run is whole-number arithmetic (§6.1). + +OpenQASM 3 schedules an `extern` call and lets it run concurrently with other +operations. This profile fixes one schedule: a call runs when the program reaches it, in +program order, and completes before the next statement; it is atomic. A program read +with any other reader of this repository keeps refusing an `extern` call (`Q125`). + +### 5.2 Lowering + +A callout has one of two forms, by its widths, as [`quantum-wasmq.wit`](quantum-wasmq.wit) +declares them: + +- **scalar**, when every argument and the result have at most 64 bits: each lowers to one + `i64` (WIT `u64`), bit `i` of the register bit `i` of the integer, an `int[k]` + sign-extended. The export's type is `(i64, …) -> (i64)`, or `-> ()` with no result. +- **list**, otherwise: each `bit[n]` argument lowers to a `list` (an `i32` pointer and + an `i32` length) and the result to one `i32` that points to a pointer and a length; + `uint[k]` and `int[k]` arguments stay `i64`. The list's bytes are `ceil(n/8)` long, bit + `i` in byte `i/8` at bit `i % 8`, the rest of the last byte zero: the Component Model's + canonical ABI. + +A host reaches a module's memory only as its export named `memory`. A module with a +list-form callout exports it, and `cabi_realloc` when the callout has a list argument +(`W015`). The exports a host calls have exactly these types (`W012`): + +| export | type | +|---|---| +| `cabi_realloc` | `(i32, i32, i32, i32) -> (i32)` | +| `cabi_post_` | `(i32) -> ()` | +| `wasmq-seed` | `(i64) -> ()` | +| `wasmq-prior` | `(i32, i32, i32) -> ()` | +| `init` | `() -> ()` | + +A host makes a callout in these steps, each guest call under the callout's one limit, +its stack's weight starting at zero: + +1. For each list argument, in order: it calls `cabi_realloc(0, 0, 1, len)`, charges + `len` fuel, and writes the bytes at the address returned. +2. It calls the export. +3. For a scalar result: a bit set at or above the result's width ends the run. +4. For a list result at address `r`: `r` is a multiple of 4; it reads a pointer and a + length at `r`; the length is `ceil(n/8)`; it charges that many fuel and reads the + bytes; no bit past the width is set in the last byte; then it calls + `cabi_post_(r)` if the module exports it. + +A step that cannot be done as written (bytes that do not fit in the memory at an +address, an address not a multiple of 4, another length, a bit past the width) ends the +run with `bad_result`, with the fuel used up to that step. + +### 5.3 Lifecycle + +Instances are made at the start of a shot, before its first step, one for each module in +the contract's order (each shot when `state` is `shot`, once at the first shot when it is +`job`); a fault in making one ends the run there, and no later module's is made. When an +instance is made, the host calls, in this order and each if the module exports it, each +under the limit `per_instance`: + +1. `wasmq-seed(seed: u64)`, with the run's seed, or, when `state` is `shot`, the shot's + seed (§6.1); +2. `wasmq-prior(index: u32, bytes: list)` for each of the module's priors, in the + contract's order, the bytes passed as a list argument is (§5.2, step 1); +3. `init()`. + +A module with priors exports `wasmq-prior`, `cabi_realloc` and its memory (`W015`). + +### 5.4 Priors + +A prior is a parameter file a module reads: a code's parity checks, a target register, a +noise model, a calibration. It travels in the envelope and is pinned in the contract, so +a module is never run against another prior than the one it was packed with (`W014`). + +## 6. A run and its receipt + +### 6.1 Shots and outcomes + +A run goes shot by shot, in order. Each shot starts from `|0…0⟩` with every bit 0, and +has a **stream**: the outputs of SplitMix64 from the state `s XOR 0x51574d31`, where `s`, +the shot's seed, is SplitMix64's output for the state +`seed + (shot + 1) · 0x9e3779b97f4a7c15` (all sums modulo 2^64). + +A measurement of a qubit in the computational basis has an outcome that is determined by +the state, or is 0 or 1 with equal chance; with §5.1's gates there is no other case. A +determined outcome is the outcome, and takes nothing from the stream. A random outcome +takes the stream's next value `x`, and is 1 when `x < 2^63`. A `reset` measures its qubit +the same way, without writing a bit, and applies `x` to it when the outcome is 1; a +reset of a qubit whose outcome is determined takes nothing from the stream. + +A shot's outcome is its program's bits, `n` of them: every bit it declares at its top +level, whether or not anything writes it, numbered in the order the program declares them +(its first declared bit is bit 0, a register's bits in their order) and written bit +`n − 1` first; the counts sum the shots that read each outcome. A bit that a block or a +subroutine declares is its own (`Q322`): the steps read and write it, and no outcome holds +it. Each measurement names the bit it writes: a `measure` with no target bit is refused +in a program that calls externs (`Q324`), since its outcome is the bits it declares. A +program that measures nothing has its bits as its outcome all the same, those its calls +write and 0 for each bit nothing writes: no qubit is read for it. + +### 6.2 Faults + +A run stops at the first fault, seals what it ran, and reports: + +| reason | | +|---|---| +| `fuel_exhausted` | a charge took the fuel below zero (§4.2); `budget` says whose limit it was: `call`, `instance`, or `run` when the limit was what was left of the run's | +| `stack_exhausted` | §4.3 | +| `unreachable`, `memory_out_of_bounds`, `table_out_of_bounds`, `indirect_call_null`, `indirect_call_type`, `integer_divide_by_zero`, `integer_overflow`, `invalid_conversion_to_integer` | the module trapped (WebAssembly 3.0, §4 Execution) | +| `bad_result` | §5.2 | + +A fault has the members `reason`, `shot`, `phase` (`seed`, `prior`, `init` or `call`), +`module` (its name), `fuel` (what the stopped call used), `callout` (the `extern`'s +name) when the phase is `call`, and `budget` when the reason is `fuel_exhausted`, and no +other. The counts are of the shots that finished. A fault receipt is a receipt like any +other: it verifies, and running the envelope again reproduces it. + +### 6.3 What is never sealed + +A host that could not run the bundle seals nothing: a refused envelope (§2.3, §3.3), a +host that cannot hold the memory's maximum or grow within it (§4.7), an engine that +fails for its own reasons. These are the host's condition, not the bundle's, and a +receipt that recorded them would differ by host. Nothing a bundle holds can lead here: +what a bundle could cause is refused on reading (§2.3, §4.7, §4.8) or sealed as a fault. + +### 6.4 The result + +The job body is the engine's (`engine::job`), with exactly four members: +`{ "status", "method", "request", "result" }`. The request is +`{ "wasmq": , "envelope_blake3" }`. `status` is `done` when the result's +`status` is `ok` and `fault` otherwise; `method` is the result's. The receipt signs the +request and the result, as [the provider protocol's §8.2](quantum-providers.md#82-how-it-is-sealed-and-how-a-job-is-read) +seals every job; `status` and `method` only repeat what is signed. They are the host's +word, unsealed: a verdict takes nothing from them, and a check notes where they, or any +other member of the body, differ from what is sealed (§6.5). + +The result has exactly these members: + +| member | | +|---|---| +| `format` | `"wai.quantum.wasmq.result/1"` | +| `profile` | `"wai.quantum.wasmq/1"` | +| `status` | `ok`, or `fault` when the run stopped | +| `method`, `qubits`, `shots`, `seed` | the contract's method, the program's qubits (§2.2), the contract's shots and seed | +| `shots_run` | the shots that finished | +| `counts` | outcome to shots (§6.1) | +| `callouts` | the callouts made, one that stopped the run among them | +| `fuel` | `{ "used", "instances", "budget": { "per_call", "per_job", "per_instance" }, "callouts": { name: { "calls", "fuel", "max" } } }`: all the fuel used, the part of it lifecycle calls used, the contract's budgets, and for each `extern` the run called, its calls, their fuel and the most one of them used (an `extern` it did not call has no entry) | +| `modules` | for each, in the contract's order: `{ "name", "blake3" }` | +| `transcript_blake3` | BLAKE3 over `wai:wasmq-transcript\x01` and, for each callout that returned, in order: its shot (u64), the index of its `extern` (u32), its input's length (u32) and bytes, its output's length (u32) and bytes, and its fuel (u64), little-endian, the input the arguments' bits in order and the output the result's, packed as a list's bytes are (§5.2); after a fault, the bytes `fault`, the shot (u64), the reason's bytes and the fault's fuel (u64) | +| `fault` | §6.2, when the run stopped | +| `accuracy` | `{ "class": "exact" }` | +| `energy` | `{ "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" }` | +| `receipt` | the engine's job receipt (`engine::receipt`): Ed25519 over BLAKE3 of the request and the result in RFC 8785 form | + +**Result-equivalence.** Every conforming runner, on every host and every engine, gives +an envelope the same result: the same bytes in its RFC 8785 form, the receipt aside. So +the receipt's digest is the same everywhere, and under one key the whole job is. + +### 6.5 Verifying + +A wasmQ job is a job like any other: every verifier of the engine's job receipts reads it +and judges it by [the provider protocol's §8.4](quantum-providers.md#84-what-a-verifier-checks), +and so gives it one verdict, wherever it is checked. `wai-quantum verify` (and its +`--json` and `--lines`), the job service's `POST /v1/verify`, both MCP servers given the +job's text, the Python package, the desktop app and a page read the job from its text as +§8.2 reads every job's, so its seal and its numbers are held as every receipt's are. A +surface given the job already parsed (a view, an MCP server given its members) cannot see +what parsing loses, as the provider protocol's §9 says, and gives every other reason; the +Python package judges a dict as the text it writes for it. A result +whose `format` is `wai.quantum.wasmq.result/1` is a wasmQ job's, and §8.4 adds one reason +for it, `wasmq_form`: the job breaks a rule below. Only what is sealed (the request, and +the result less its receipt) and the form of its seal decide it. A signature shows that a key sealed a job and says +nothing of what the job holds, so without the envelope to run again these rules are what +hold a result to its contract and to itself: a key can seal any result of this form, and +only running the envelope again shows that a result is the one it gives. + +In these rules a number's value is the double it reads as (§8.2). A *count* is a number +whose value is a whole number from 0 to 2^53 − 1, however it is written (`1`, `1.0` and +`1e0` are one count, sealed as one); "at most" and "equals" compare counts, and a rule on +a count fails when the number is not one. A *digest* is a string of 64 lowercase +hexadecimal digits. Two values are *equal* as JSON values are, numbers by their value and +an object's members in any order. "Exactly the members" means every one named and no +other; a value that is not an object has none. A member is *present* when its object names +it, whatever its value, `null` among them: "has no `fault`" and "has a `callout`" read +so. A list or a map named below that is not an +array or an object is read as an empty one, after its own rule has failed. `C` is the +request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F` is +`r`'s `fuel`. *The budget* `per_call`, `per_job` or `per_instance` is that member of `run`'s +`fuel`. The job breaks the form when any of these fails: + +1. No number in the request, or in the result less its receipt, has a magnitude past + 2^53 − 1. +2. The request has exactly the members `wasmq` and `envelope_blake3`, and + `envelope_blake3` is a digest. +3. `C` has exactly the members §2.2 gives: `profile`, `program`, `modules`, `callouts` + and `run`; its `program` exactly `blake3`, `bytes` and `inputs`; `run` exactly + `shots`, `seed`, `method`, `state` and `fuel`; and `run`'s `fuel` exactly `per_call`, + `per_job` and `per_instance`. +4. `C`'s `profile` is `wai.quantum.wasmq/1`. +5. The program's `blake3` is a digest; its `bytes` a count from 1 to 2^20; its `inputs` an + object each of whose values is a number whose value is whole and at most 2^53 − 1 in + size. +6. `C`'s `modules` is an array that is not empty. Each module has exactly the members + `name`, `blake3`, `bytes` and `priors`: `name` a string, `blake3` a digest, `bytes` a + count of at least 1, and `priors` an array each of whose priors has exactly the members + `name` (a string), `blake3` (a digest) and `bytes` (a count). The modules' `bytes` that + are counts add up to at most 2^19, so each is at most that; rule 31 bounds a prior's. +7. `C`'s `callouts` is an array that is not empty. Each callout has exactly the members + `extern`, `module` and `export`: `extern` and `export` strings, and `module` a count + less than the number of modules. No two callouts have the same `extern`, and each + module's index is the `module` of a callout. +8. `run`'s `shots` is a count from 1 to 2^20, its `seed` a count, its `method` + `stabilizer`, its `state` `shot` or `job`, and each of its fuel's three budgets a count, + `per_call` and `per_instance` each of at most 2^39 (§4.8; rule 30 holds `per_job` to + less). +9. `r` has exactly the members of §6.4 (`format`, `profile`, `status`, `method`, + `qubits`, `shots`, `shots_run`, `seed`, `counts`, `callouts`, `fuel`, `modules`, + `transcript_blake3`, `accuracy`, `energy` and `receipt`), and may have `fault`. +10. The receipt is an object whose `canon` is `rfc8785`: the first form (§8.2) is older than + this profile, and no host sealed a wasmQ job in it. +11. `r`'s `format` is `wai.quantum.wasmq.result/1`, its `profile` `wai.quantum.wasmq/1`, + its `accuracy` `{ "class": "exact" }` and its `energy` the one §6.4 gives; and its + `method`, `shots` and `seed` equal `run`'s. +12. `r`'s `qubits` is a count from 1 to 2^15 (a program declares a qubit at least, `Q100`), + and its `transcript_blake3` a digest. +13. `F` has exactly the members `used`, `instances`, `budget` and `callouts`; its `budget` + equals `run`'s `fuel`; and `used` is a count of at most the budget `per_job` (0 when + that is not a count). +14. `F`'s `callouts` is an object. Each of its members is named by the `extern` of one of + `C`'s callouts, and has exactly the members `calls`, `fuel` and `max`: `calls` a count + of at least 1; `max` a count of at most the budget `per_call` (0 when that is not a + count) and at most its own `fuel`; and `fuel` a count (rule 20 bounds it by its calls + and their `max`). +15. `F`'s `instances` and the `fuel` of each member of `F`'s `callouts`, added, are `F`'s + `used`; and the `calls` of each member, added, are `r`'s `callouts`. Each of them is a + count. +16. `r`'s `modules` has as many entries as `C`'s, each with exactly the members `name` + and `blake3`, its `blake3` a digest, and its `name` and `blake3` equal to those of + `C`'s module at its index. +17. `F`'s `instances` is a count of at most M × S × the budget `per_instance` (0 when that + is not a count): S is the sum, over `C`'s modules, of 2 and the number of its priors + (each instance's seed, its init and each prior, §4.2, §5.3); M, the instances made, is 1 + when `run`'s `state` is `job`, whatever the status; for any other `state` it is `r`'s + `shots_run` (0 when it is not a count), plus 1 when `r`'s `status` is `fault`. +18. When `r`'s `status` is `ok`: `r` has no `fault`, and its `shots_run` equals its + `shots`. When it is `fault`: the fault has exactly the members `reason`, `shot`, + `phase`, `module` and `fuel`, and may have `callout` and `budget`; `reason` is one §6.2 + names; `shots_run` is less than `shots`; `shot` equals `shots_run`; `phase` is `seed`, + `prior`, `init` or `call`; `module` equals the `name` of one of `C`'s modules; `fuel` + is a count, and in a phase other than `call` at most the budget `per_instance` (0 when + that is not a count: a call's is held by its callout's `max`, below, and rule 14; rules + 20 and 25, with rule 15, hold either within `F`'s `used`); it has a + `callout` exactly when its phase is `call`, and that is the `extern` of one of `C`'s + callouts that names a member of `F`'s `callouts` whose `max` is at least the fault's + `fuel` (the call that stopped is one of the calls made, §6.4); and it has + a `budget` exactly when its reason is `fuel_exhausted`, and that is `run`, or `call` in + a call and `instance` otherwise. Any other `status` breaks the form. +19. `r`'s `counts` is an object; each of its names holds only `0` and `1` (the empty name + among them), all of one length; each of its values is a count of at least 1; and its values, added, are `r`'s + `shots_run`. + +Rules 20 to 35 tie members to each other, as the writer does. In them *the fault* is `r`'s +`fault` when `r`'s `status` is `fault` and that is an object, and there is none +otherwise; a rule about the fault holds when there is none. A *call's fault* is a fault +whose `phase` is `call`, and a *lifecycle fault* one that has a `phase` other than `call`. + +20. For each member of `F`'s `callouts`, let `s` be 1 when the fault is a call's fault + whose `callout` is that member's name, and 0 otherwise, and `e` the fault's `fuel` when + `s` is 1 (0 when that is not a count) and 0 otherwise. `T`, its `fuel` less `e`, the fuel + of its calls that returned, is not less than 0, and is from `k` to `k` × `max`, where `k` + is its `calls` less `s` (0 when that is less than 0): the call that stopped used the + fault's fuel, and each call that returned used at least 1 (§4.2: entering a function + weighs 1) and at most the most one call used. When `max` is more than `e`, the most was a + call's that returned, and each of the others used at least 1, so `T` is also at least + `max` + `k` − 1. `calls`, `fuel` and `max` are counts. +21. `F`'s `instances` is a count of at least `W` × `P` + `E`, the least that making the + instances took outside a stopped call (§5.3: a shot's instances are made module by + module in `C`'s order, each one's `wasmq-seed`, then its priors, then its `init`; passing + a prior calls `cabi_realloc`, at least 2 since it returns a value, then charges the + prior's bytes, then calls `wasmq-prior`, at least 1, §4.2, §5.2): + - `P`, what each set of instances took at least: the sum, over every prior of every one + of `C`'s modules, of 3 and the prior's `bytes` (0 when that is not a count), and 1 + more when the fault is a lifecycle fault in phase `seed` or `init`: its module exports + that function, and each set called it; + - `W`, the sets made whole: `r`'s `shots_run` (0 when that is not a count), plus 1 when + there is a call's fault; when `run`'s `state` is `job` it is that or 1, whichever is + less; + - `E`, when the fault is a lifecycle fault, what the set it stopped took at least: the + same sum, of 3 and its `bytes`, over the priors of each module before the one the fault + names (the first of `C`'s modules with that `name`), and, when its phase is `init`, over + that module's own; 0 otherwise, and when no module has that `name`. +22. A call's fault's `module` equals the `name` of `C`'s module whose index is the + `module` of each of `C`'s callouts whose `extern` is the fault's `callout` (rule 7 + allows one): a call runs in its callout's module (§2.2, §6.2). It fails when there is + no such callout, or no such module. +23. A fault whose `phase` is `prior` has as its `module` the `name` of one of `C`'s + modules whose `priors` is not empty: a prior is passed only to a module that has one + (§5.3). +24. A fault whose `reason` is `bad_result` has the `phase` `call` or `prior`: only these + pass bytes or take a result (§5.2). +25. A lifecycle fault's `fuel` is a count, and `F`'s `instances` (a count) is at least it + added to rule 21's `W` × `P` + `E`: what the stopped lifecycle call used is part of the + instances' fuel, beside what the calls before it took. +26. When `run`'s `state` is `job`, a lifecycle fault's `shot` is the count 0: the run's + one instance of each module is made at the first shot (§5.3). +27. A lifecycle fault whose `shot` is the count 0 comes with `r`'s `callouts` the count 0: + the first shot's instances are made before its first step (§5.3). (Rule 32 holds this + too, so rule 27 is never broken alone.) +28. A fault whose `reason` is `fuel_exhausted` used its whole limit (§4.2). When its + `budget` is `call`, its `fuel` is a count equal to the budget `per_call`; when it is + `instance`, to the budget `per_instance` (each 0 when it is not a count). When it is + `run`, `F`'s `used` is a count equal to the budget `per_job` (a count), and the fault's + `fuel` is a count, less than its own limit: the budget `per_call` in a call's fault, and + the budget `per_instance` in a lifecycle fault, each read as 0 when it is not a count (a + fault with no `phase` has no limit here, and breaks rule 18): the run's budget was the + limit only because less of it was left than the call's own. +29. When `r`'s `counts` has a name, the length of the longest, in characters (Unicode code + points), is at most 2^20, and `run`'s `shots` (0 when that is not a count) times that + length at most 2^28: a program holds at most 2^20 bits, and a run's shots times them are + at most 2^28 (§4.8). (Rule 19 holds each name to `0` and `1`, one byte each; a verifier + counts a name that breaks it in characters too.) +30. The part of a run's work that a receipt shows is at most 2^42 (§4.8): + `S` × 2·`n`·`w` + `I` × Σ (4096 × (1 + 2 × `p`) + `b`) + 131072 × Σ `b` + 4097 × `K` + 8 × + the budget `per_job`. `S` is `run`'s `shots`; `n` is `r`'s `qubits`; `w` is ⌈`n`/32⌉, or + 1 when that is 0; `I` is 1 when `run`'s `state` is `job` and `S` otherwise; each sum is + over `C`'s modules, `p` a module's number of priors and `b` its `bytes`; and `K` is `r`'s + `callouts`. Every number here that is not a count is read as 0. The host's work is at + least this: each shot's fresh tableau, each instance's module and its calls of + `wasmq-prior` and of `cabi_realloc` for it, the run's compiling, each call made (its + module's part, 4096, and its step, 1: a callout's step is counted in each shot whether or + not it runs) and the run's fuel. +31. The envelope the contract names is at most 2^27 bytes (§2.3): 20 bytes of magic and + lengths, its manifest (§2.1's members, in that order and with no space: 192 bytes and the + decimal digits of its `qubits`, `r`'s, and its `shots` and `seed`, `run`'s, each read as 0 + when it is not a count), `C` in RFC 8785 form (§2.2; no bytes when the request has no + `wasmq`), and its frames (the program's `bytes`, each module's `bytes` and each prior's + `bytes`, each 0 when it is not a count), added, are at most 2^27. +32. `r`'s `callouts` is at most 2^21 × `H` (each 0 when it is not a count), where `H`, the + shots that ran steps, is `r`'s `shots_run` and 1 more when there is a call's fault: a + shot runs each of its steps at most once, every jump going forward, and a program has + at most 2^21 steps (§4.8), each call one of them. +33. Each prior passed whole fits within one lifecycle call's limit: 3 and its `bytes` (0 when + that is not a count) are at most the budget `per_instance` (0 when that is not a count), + for every prior of every one of `C`'s modules when `W` (rule 21) is at least 1, and for + each prior `E` counts otherwise. Passing a prior is one stretch under one limit: + `cabi_realloc`, its bytes and `wasmq-prior` (§5.2, §5.3). +34. A fault's `fuel`, when it is a count, is at least what its `reason` shows was charged + (§4.2), when it has one: 1 for every reason but `fuel_exhausted`, one §6.2 does not name + or `null` among them (a call stops any other way only after its entry was charged); 2 + for `bad_result` (a value came back: the entry, and an instruction that made it); and 788 + for `stack_exhausted`. A frame weighs at most + 576 + 4096 + 2^8 + 2^14 = 21 312 (§3.2, §4.3), so the stack's weight passes 2^23 only at + a call in the 394th frame entered or deeper, and each frame entered was charged at least + 2: its entry, and the run that holds its call. +35. When no call returned, the transcript is what §6.4 gives such a run: when `r`'s + `callouts` is 0, or 1 and the fault is a call's fault (each a count), `r`'s + `transcript_blake3` is BLAKE3 over the bytes `wai:wasmq-transcript` and the byte 0x01 + and, when there + is a fault, the bytes `fault`, the fault's `shot` (u64), its `reason`'s UTF-8 bytes and + its `fuel` (u64), little-endian (`shot` and `fuel` each 0 when it is not a count, and + `reason` no bytes when it is not a string). + +**What the rules leave free, and why.** What else the writer fixes, the rules leave free, +each for the reason given, and only running the envelope again (`--bundle`) holds a job to +it: + +- the `transcript_blake3`'s value once a call returned: it digests each call's input and + output, which no receipt holds (rule 35 holds it when none returned); and each `blake3` + against the bytes it pins, which no receipt holds; +- `r`'s `qubits` within rule 12, the outcomes' length within rule 29, and the number of + callouts within rules 30 and 32, against the program: its text is not sealed, only its + digest and length, so how many qubits and bits it declares, and how many calls each shot + makes, are its own; +- which shot read which outcome, and the counts' spread: only the run shows them; +- whether a module exports `wasmq-seed` or `init`, but for the one a fault in that phase + names (rule 21): only its code shows it. So a fault in phase `seed` or `init` may name any + module; +- the instances' fuel within rules 17, 21 and 25: what each lifecycle call used past its + least, and how far the module a fault names got, are its code's; +- the fuel of each call within rules 14, 18, 20, 28 and 34, and a fault's fuel within rules + 18, 20, 21, 25, 28 and 34: where in its work a call traps, and how many bytes its arguments + pass, are its module's and its program's; +- a trap's `reason` against its phase: any guest call can trap any way, and only + `bad_result` is the host's (rule 24); +- the rest of §4.8's bounds (the steps of a shot past rule 32's count, the gates and + measurements, the bits of calls, the pages a module starts with, the program's reading): + each counts what only the program or the modules hold, so rules 29 to 32 take what a + receipt shows, and a job a writer can write never breaks them; +- the least length of a program and of a module, and the spelling of an `extern`: a program + that packs declares an `extern`, a qubit and a call, a module that packs has a type, a + function, an export and its code, and an `extern` is a name the reader reads, its letters + any of Unicode's. So each has a least length, and an `extern` a form, that a rule could + state; but they follow from the grammar, WebAssembly's binary format and Unicode's tables + rather than from anything a run does, bound nothing a host does, and a rule at them would + hold every verifier to those tables. Rules 5, 6 and 7 hold the program's and a module's + `bytes` to at least 1 and an `extern` to a string, and leave the rest free. + +Beside §8.4's reasons a verifier of the engine states, for a wasmQ job, `wasmq`: +`outcome`, the sealed result's `status`; `problems`, what breaks each rule the job breaks, +in words, each naming its rule first (`rule 19: ...`), the first 64 of them and then how +many more; and `notes`, where the host's unsealed words +differ from what is sealed or are missing ("the host's unsealed status says `done`; the +sealed result says `fault`"). A note is no part of the verdict: the verdict is the sealed +content's alone, so that every surface that checks a job gives the same one. + +`wai-quantum verify JOB.json` prints the receipt's check, the outcome and each problem and +note; `--json` gives the verdict as JSON. `--lines` reads a file of one job a line, each +line on its own as a job's text (a line that is not UTF-8, or blank, is that line's +`unreadable`; the newline that ends the file ends its last line), and gives one verdict a +line, each a job's whatever its kind; a file with no line is refused. A job found +unreadable, no job, with no receipt, or longer than the surface reads (`too_large`) is +reported alone (§8.4), whatever kind of job it says it is: `verify` says that its form was +not judged, and runs no envelope for it. `verify` reads at most 384 MiB and 2^22 values +(§8.4), more than any job holds, in about 1 GiB of memory at most on every host: a file past the +bytes is `too_large` by its size, before it is read, and one past the values once read, +before it is parsed; a file of lines past the bytes is refused, and a line past the values +is that line's `too_large`. With `--bundle ENVELOPE.wai` it also checks +that the envelope is the one the receipt binds (its BLAKE3 is the request's +`envelope_blake3`, and its contract is `C`), and runs it again here: the result must be +the same, byte for byte, the receipt aside; with `--json` the verdict then also says +`bundle` and `reproduced`, and is valid only when both hold. A wasmQ job carries no +history, so `--history` is refused for one. + +## 7. On hardware + +**The bring-your-own-quantum boundary is unchanged.** WAI Quantum never pays for or +resells hardware time; a person's providers speak `wai.quantum.provider/1` +([quantum-providers](quantum-providers.md)), whose program is a fixed sequence of native +gates measured at the end. A wasmQ bundle calls its modules between gates, which that +protocol does not carry, so `run ENVELOPE.wai --provider NAME` is refused, and the hosted +job service takes no bundle. A wasmQ run simulates the quantum part on this machine. + +**Nothing here has run a module beside a quantum processor.** This repository has run +wasmQ modules only on the hosts its tests name. What holds for a module on a machine +that runs classical code between gates, and what does not: + +- A scalar-form module (§5.2) is a function of integers that imports nothing. Whether a + given machine takes it is that machine's to say. +- The quantum part's measurements on hardware are not reproducible, so a hardware run + has no single result to reproduce. Its classical part does: given the inputs each call + received, a module gives the same outputs and the same fuel on every host, so a + record of the calls (`run --transcript`) replays through the modules alone and is + checked call by call (`run ENVELOPE.wai --replay TRANSCRIPT`), each of a shot the run + has, in the order of its shots. Each call shows only itself; that a record is the whole + of a run's shows in its digest, which a replay states and the receipt of a run that ended + `ok` seals as `transcript_blake3`. A machine that ran the module would have to report + those inputs and outputs for anyone to check them. +- Fuel is not time (§4.2). A real-time deadline is the machine's; the profile states + the work. + +## 8. Reference modules + +Two modules of record are built from code the engine already has, for +`wasm32-unknown-unknown` with Lime1 (`wai-wasmq/modules/`, `build.sh`), and committed in +[`../quantum-conformance/wasmq/modules/`](../quantum-conformance/wasmq/modules/) with +the digests of the sources that made them. rustc's wasm32 output depends on the host it +runs on, so they are built on x86_64 Linux, where CI rebuilds them and compares. + +### 8.1 The qLDPC decoder + +`quantum_qec`'s decoders: union-find matching for a code whose qubits each lie in at most +two checks (the surface code), Relay-BP in fixed-point arithmetic otherwise (the +bivariate-bicycle codes), as the engine itself chooses. Prior 0 is a code's parity +checks in `CssCode::check_matrix_bytes`'s form. Exports: `decode` (list) and `decode64` +(scalar). Tests hold every result to the same functions compiled natively, on every +weight-two error of the distance-3 surface code and on random errors of the [[72,12]] +bivariate-bicycle code (`CssCode::gross()`), and run memory rounds of the distance-3 and +distance-5 surface codes and of that code (a 129-qubit circuit) with the decoder called +between the syndrome and the correction, each shot ending in the code, on every engine +with the same bytes. + +### 8.2 The atom rearrangement planner + +`quantum_atom::solve`, an exact Hungarian assignment of loaded atoms to target sites. +Prior 0 is the target register in `target_bytes`'s form. Exports: `plan` (list) and +`plan64` (scalar). A circuit loads each trap with probability ½, measures the occupancy, +calls the planner, applies its moves and reads the array: every shot whose plan is +feasible fills the target, and atoms are moved, never made or lost. + +## 9. Conformance + +[`../quantum-conformance/wasmq/`](../quantum-conformance/wasmq/) holds: + +- `envelopes/`: envelopes, each with the result it MUST give on every engine and every + host (`vectors.json`, `runs`): the status, the fault, the fuel, the transcript's + digest, the result's digest, the receipt's digest and, under the vectors' key, the + whole job's digest. Among them are runs that end in every fault reason §6.2 names, in + every phase and of every budget; callouts of the list form, with a `cabi_post` call and + with each way a list result is not one; a signed argument; two modules made in order; a + program that declares no qubit and names physical ones, its bits in two registers; a + program that measures nothing; one with bits nothing writes, and one whose block holds a + bit of its own, which pin §6.1's outcome; and a program whose resets and measurements pin + §6.1's stream; +- `refuse/`: modules and envelopes a runner MUST refuse, each with its code + (`vectors.json`, `refuse`), at least one for every code of §3.3, each one defect away + from a module or an envelope that is accepted; the modules also as WebAssembly text + where they have one. An envelope past §2.3's size has no vector, for its size: a test + in `wai-wasmq/tests/sweep.rs` makes one; +- `modules/`: the modules of record (§8). + +Six wasmQ jobs the command line sealed are among the job receipts of +[`../quantum-conformance/receipts/`](../quantum-conformance/receipts/) (`jobs/cli-wasmq-*`): +a run of the surface-code round, one of a single instance for the whole run, and faults in +a call, of the run's budget, in an instance's init and in a prior. Beside them are jobs +crafted from them, sealed again, each of rules 2 to 31 broken alone by one at least, so the +two verifiers of that corpus, which share no code, are held to each other on `wasmq_form` +and on the rules each states broken, and on every change of one member of each of the six. + +`wasmq_vectors verify wai/quantum-conformance/wasmq` checks them on every engine of the +build and prints a digest of what it computed (each run's job and each refusal's code), +the same on every host that conforms. + +## 10. Versioning + +`wai.quantum.wasmq/1` is this text. A change to anything that moves a result's bytes or +a module's fuel (the weights, the runs, the stack's weights, the lowering, the stream, +the transcript, the result's members) is a new profile version, and a receipt names the +version it was made under. + +This version leaves out, and a later one may add under a name of its own: + +- gates that take an angle and gates outside the Clifford group, which need a method + whose arithmetic this text would have to fix bit for bit. The state-vector method, + which the engine runs in floating point, is not part of this version for that reason: + `stabilizer` is its one method (§2.2); +- floating-point arguments and results of a callout; +- noise, and observables other than the counts. diff --git a/wai/extensions/quantum-wasmq.wit b/wai/extensions/quantum-wasmq.wit new file mode 100644 index 000000000..e46134c71 --- /dev/null +++ b/wai/extensions/quantum-wasmq.wit @@ -0,0 +1,61 @@ +// The WebAssembly interface of a wasmQ module (quantum-wasmq.md). +// SPDX-License-Identifier: Apache-2.0 +// +// A wasmQ module is a core WebAssembly module (quantum-wasmq.md §3). Its exports are the +// Component Model's canonical lowering of the functions below, so a component host can +// wrap it, and a host of this profile calls it directly. An OpenQASM 3 `extern` lowers to +// one of two forms (§5.2): scalar, every argument and the result at most 64 bits, each a +// u64 or s64; or list, each `bit[n]` a list, the lowest bit first. A module with a +// list-form export or a prior exports its memory as `memory`, where a host reaches it. +// +// The direction is the hardware's: the quantum program calls the module. A module +// imports nothing (§3.2), so this package has no interface a module imports. + +package wai:wasmq@0.1.0; + +/// What a receipt names when a run stops (§6.2). +interface receipt { + enum fault-reason { + unreachable, + memory-out-of-bounds, + table-out-of-bounds, + indirect-call-null, + indirect-call-type, + integer-divide-by-zero, + integer-overflow, + invalid-conversion-to-integer, + fuel-exhausted, + stack-exhausted, + bad-result, + } +} + +/// A module's lifecycle (§5.3), every export optional: the seed, then each prior in the +/// order the contract lists them, then `init`, each metered. +world module { + /// The seed for this instance: the run's, or the shot's when an instance lives for a shot. + export wasmq-seed: func(seed: u64); + /// Prior `index` of the module's priors, its bytes as the contract pins them. + export wasmq-prior: func(index: u32, bytes: list); + export init: func(); +} + +/// The reference decoder (§8.1): the engine's qLDPC decoder. Prior 0 is a code's parity +/// checks; a syndrome goes in, a correction comes out. +world qldpc-decoder { + include module; + /// The list form: `extern decode(bit[m]) -> bit[n];` for any code. + export decode: func(syndrome: list) -> list; + /// The scalar form: `extern decode64(bit[m]) -> bit[n];`, m and n at most 64. + export decode64: func(syndrome: u64) -> u64; +} + +/// The reference planner (§8.2): the engine's neutral-atom rearrangement. Prior 0 is the +/// target register; an occupancy goes in, the moves come out as masks. +world atom-planner { + export wasmq-prior: func(index: u32, bytes: list); + /// The list form: `extern plan(bit[n]) -> bit[2n+1];`. + export plan: func(occupancy: list) -> list; + /// The scalar form, for at most 31 sites. + export plan64: func(occupancy: u64) -> u64; +} diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 98e352ef3..7b333e80f 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -20,7 +20,9 @@ The OpenQASM 3 reader's corpus, programs with what the reader must do with each, is `qasm3/`, with a README of its own. The error-correction text formats (noisy Clifford circuits and detector error models, read and written back byte for byte, with what each must count and what must be refused) are `qec-text/`, -with a README of its own. Programs in other formats (QIR under its profiles, +with a README of its own. Programs that call WebAssembly between their gates (`wai.quantum.wasmq`), the +reference modules of record and what a runner refuses are `wasmq/`, with a README of its +own. Programs in other formats (QIR under its profiles, and jeff), each with the program it must read to, its round trip and its refusals, and OpenQASM written to each, are `formats/`, with a README of its own. diff --git a/wai/quantum-conformance/receipts/README.md b/wai/quantum-conformance/receipts/README.md index 605f35a8e..f57e23e06 100644 --- a/wai/quantum-conformance/receipts/README.md +++ b/wai/quantum-conformance/receipts/README.md @@ -8,11 +8,12 @@ exactly the reasons the manifest names (§8.4): none for a valid one. |---|---| | `manifest.json` | every receipt in `jobs/`, the reasons a verifier must give it, how it was made, and what it is. Every expectation is written by hand from §8.4; none is copied from a verifier's output | | `jobs/cli-*.json` | made by the command line on this machine, sealed with the published demo key (`make.sh`): runs by several methods, a compile, and runs on the reference provider over a local process, free and priced | +| `jobs/cli-wasmq-*.json` | wasmQ jobs ([quantum-wasmq](../../extensions/quantum-wasmq.md)) made by the command line from the profile's own envelopes (`../wasmq/envelopes/`) with the demo key (`make.sh`): a run of the surface-code round, one of a single instance for the whole run, and faults in a call, of the run's budget, in an instance's init and in a prior | | `jobs/service-*.json` | made by the free job service, sealed with its key (`make.sh`) | | `jobs/scripted-*.json` | provider jobs of every built-in kind (`sampler-v2`, `prx-circuits`, `native-turns` with and without a noise model, `cloud-tasks` on a processor and on its simulator, with rows of shots and with probabilities), each through a service that follows that provider's published contract, sealed with the demo key (`wai-quantum-qpu/tests/clients.rs`, `every_built_in_kind_states_what_it_sent_and_its_receipt_keeps_every_rule`, with `WAI_QUANTUM_RECEIPT_CORPUS` set to this directory) | | `jobs/first-form-*.json` | sealed by the job service as it was on 18 October 2026 (commit 72d18742d), so by the first form's own writer, not by a restatement of it | | `jobs/fixture-*.json` | the engine's pinned fixtures, sealed by earlier engines: a run before results named their engine, a provider job of the earlier form, and a compile | -| `jobs/crafted-*.json` | made by `generate.py` from the receipts above, each changed one way: the seal's form, the strict signature (a weak-key forgery, `S + ℓ`, a mixed-order key, …), the first form, what any JSON reader and writer may do (and so must change nothing), the strict reader, and each rule of a provider's result, each sealed again with the demo key where the change is inside the seal so that one rule alone breaks | +| `jobs/crafted-*.json` | made by `generate.py` from the receipts above, each changed one way: the seal's form, the strict signature (a weak-key forgery, `S + ℓ`, a mixed-order key, …), the first form, what any JSON reader and writer may do (and so must change nothing), the strict reader, and each rule of a provider's result and of a wasmQ job's form, each sealed again with the demo key where the change is inside the seal so that one rule alone breaks | The made receipts are kept as they were made, each pinned by its digest in `made.lock.json`: a receipt missing or changed fails `generate.py --check`. A receipt made again differs in its @@ -24,7 +25,9 @@ to pass a test. A row of §8.3 that a client stops writing is held by the receip - `wai-quantum verify --json FILE` (and `--lines FILE`, one job a line): the reference, `wai_quantum::engine::job::check`. - [`../tools/receipt_verify.py`](../tools/receipt_verify.py): written in Python from §8 - alone, by a reader who had not seen the reference. Standard library only: its BLAKE3, + alone, by a reader who had not seen the reference. Its `wasmq_form`, a wasmQ job's form, + was written the same way from [quantum-wasmq §6.5](../../extensions/quantum-wasmq.md#65-verifying), + whose rules are numbered so that each can be read alone. Standard library only: its BLAKE3, Ed25519, RFC 8785 text and strict reader are its own, each held to published answers by `--self-test`. @@ -47,6 +50,35 @@ replaced) and requires the same reasons from both for every change, and two orac them: a change inside the request, the result or the receipt is never valid, and a change to the unsealed envelope, or a member added to the receipt, never changes the verdict. +A change of one member is caught by the seal before any rule of a job's form is read, so +it cannot hold the two verifiers to those rules. The rules stage does: for each rule of a +wasmQ job's form ([quantum-wasmq §6.5](../../extensions/quantum-wasmq.md#65-verifying)), +the genuine wasmQ jobs changed at the rule's bound, on both sides of it where it has two, +and sealed again with the demo key, each with the verdict written for it by hand from the +text (`rule_cases` in the differential). Both verifiers must give every case its written +verdict, so a rule dropped or a bound moved in either one fails: this stage, and the +manifest, are what back "the two verifiers agree" on the form's rules. `--second-only` +runs it for the second verifier. Each crafted wasmQ receipt that names one rule breaks that +rule alone: the reference states one problem for it, and the second verifier that rule. + +The labelled stage gives both verifiers the second verifier's own hand-labelled wasmQ cases +(`receipt_verify.py --wasmq-cases`), each as its exact text: the labels its author wrote from +the text bind the reference as they bind the second verifier, so one labelled corpus holds +both. + +[`../tools/wasmq_mutants.py`](../tools/wasmq_mutants.py) weakens each rule in each verifier, +from the code (each check dropped, each comparison moved by one, a count or a digest read +loosely, a null member read as absent and an absent one as null, the most of many read as the +least), and requires the +differential (`--wasmq-only --first`) to catch every weakening, counted per rule: + +```sh +python3 -I tools/wasmq_mutants.py python --reference PATH/wai-quantum +python3 -I tools/wasmq_mutants.py rust-schemata ../wai-quantum/src/engine/wasmq.rs mutants.json # in a scratch checkout +# build the command line from that checkout, then +python3 -I tools/wasmq_mutants.py rust --reference PATH/wai-quantum --mutants mutants.json +``` + It also changes each number of the provider receipts to values past a double's whole numbers, and sets a job's shots in both its request and its result, to see a limit one verifier has and the other does not; and it seals those of the demo key again, so that each is judged by what it diff --git a/wai/quantum-conformance/receipts/generate.py b/wai/quantum-conformance/receipts/generate.py index 3d231398a..da4259a67 100644 --- a/wai/quantum-conformance/receipts/generate.py +++ b/wai/quantum-conformance/receipts/generate.py @@ -302,11 +302,50 @@ def seal_set(body, seed=DEMO): return out +def _count(v): + """A count's value (quantum-wasmq section 6.5): a whole number from 0 to 2^53 - 1.""" + if isinstance(v, Raw): + v = v.value + if isinstance(v, bool) or not isinstance(v, (int, float)) or v != v or v in (float("inf"), float("-inf")): + return None + return int(v) if v == int(v) and 0 <= v <= 2 ** 53 - 1 else None + + +def bare_transcript(r): + """The transcript_blake3 of a wasmQ result in which no call returned (quantum-wasmq + section 6.5, rule 35): the prefix and, after a fault, its shot, reason and fuel. None + when a call returned, or its callouts are no count.""" + if not isinstance(r, dict) or r.get("format") != "wai.quantum.wasmq.result/1": + return None + fault = r.get("fault") if r.get("status") == "fault" and isinstance(r.get("fault"), dict) else None + in_call = 1 if fault is not None and fault.get("phase") == "call" else 0 + k = _count(r.get("callouts")) + if k is None or k > in_call: + return None + data = b"wai:wasmq-transcript\x01" + if fault is not None: + reason = fault.get("reason") + data += b"fault" + struct.pack(" printf 'OPENQASM 3.0;\ninclude "stdgates.inc";\ninput float theta;\nqubit[1] q;\nbit[1] c;\nrx(theta) q[0];\nc = measure q;\n' > theta.qasm "$B" run bell.qasm --program three.qasm --shots 64 --seed 3 --demo-key --json > "$OUT/cli-set-programs.json" "$B" run theta.qasm --sweep theta=0.25,0.5,1.5 --shots 64 --seed 9 --demo-key --json > "$OUT/cli-set-sweep.json" +for pair in run:surface3-decode64 state-job:surface5-decode64-job fault:fault-fuel fault-run:fault-fuel-run fault-init:fault-in-init fault-prior:fault-in-prior; do + "$B" run "$ENVELOPES/${pair#*:}.wai" --demo-key --json > "$OUT/cli-wasmq-${pair%%:*}.json" +done "$B" submit bell.qasm --shots 200 --seed 11 --json > "$OUT/service-run.json" "$B" submit bell.qasm --shots 200 -o "Z0 Z1" --json > "$OUT/service-run-stabilizer.json" for f in "$OUT"/cli-*.json "$OUT"/service-*.json; do "$B" verify "$f" > /dev/null || { echo "make.sh: $f does not verify" >&2; exit 1; }; done diff --git a/wai/quantum-conformance/receipts/manifest.json b/wai/quantum-conformance/receipts/manifest.json index 3749608c8..4abc5b469 100644 --- a/wai/quantum-conformance/receipts/manifest.json +++ b/wai/quantum-conformance/receipts/manifest.json @@ -55,6 +55,42 @@ "made_by": "made", "what": "the command line: a sweep of an input at three points, sealed as a set" }, + { + "file": "jobs/cli-wasmq-fault-init.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run that stopped in an instance's init" + }, + { + "file": "jobs/cli-wasmq-fault-prior.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run that stopped while a module read its prior" + }, + { + "file": "jobs/cli-wasmq-fault-run.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run that stopped in its third shot when the run's fuel ran out" + }, + { + "file": "jobs/cli-wasmq-fault.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run that stopped in a call past the call's fuel budget" + }, + { + "file": "jobs/cli-wasmq-run.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run, a surface-code round decoded by WebAssembly between its gates, 32 shots" + }, + { + "file": "jobs/cli-wasmq-state-job.json", + "reasons": [], + "made_by": "made", + "what": "the command line: a wasmQ run of one instance for the whole run, its decoder reading a prior" + }, { "file": "jobs/first-form-service-observables.json", "reasons": [], @@ -1162,6 +1198,354 @@ "reasons": [], "made_by": "crafted", "what": "a class a verifier does not know: not a reason, since a later version may add one" + }, + { + "file": "jobs/crafted-wasmq-counts-short.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ run whose counts add up to one shot fewer than it ran (rule 19)" + }, + { + "file": "jobs/crafted-wasmq-ok-before-all-shots.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ run that is ok after 31 of its 32 shots (rule 18)" + }, + { + "file": "jobs/crafted-wasmq-fuel-unaccounted.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ run whose instances and calls do not add up to the fuel it used (rule 15)" + }, + { + "file": "jobs/crafted-wasmq-fuel-past-job-budget.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ fault that used 400 fuel under a run's budget of 100 (rule 13)" + }, + { + "file": "jobs/crafted-wasmq-instances-past-budget.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "one instance for the whole run, given more fuel for its seed, init and prior than three stretches of 8000 (rule 17)" + }, + { + "file": "jobs/crafted-wasmq-fault-unnamed.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ fault of a reason the profile does not name (rule 18)" + }, + { + "file": "jobs/crafted-wasmq-result-member-added.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ result with a member the profile does not write (rule 9)" + }, + { + "file": "jobs/crafted-wasmq-contract-method.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract and result of a method the profile does not have (rule 8)" + }, + { + "file": "jobs/crafted-wasmq-seed-past-exact.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ seed of 2^53: a double holds it, so it is no inexact number, and the profile allows at most 2^53 - 1 (rules 1, 8)" + }, + { + "file": "jobs/crafted-wasmq-modules-other.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ result naming a module the contract does not pin (rule 16)" + }, + { + "file": "jobs/crafted-wasmq-transcript-upper.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ transcript digest in upper case (rule 12)" + }, + { + "file": "jobs/crafted-wasmq-first-form.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ job sealed in the first form, older than the profile: the seal holds, and no host wrote it (rule 10)" + }, + { + "file": "jobs/crafted-wasmq-call-fault-uncounted.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a call that stopped the run, which the fuel counts no call of: the stopped call is one of the calls made (rule 18)" + }, + { + "file": "jobs/crafted-wasmq-call-fault-past-largest.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a call that stopped after 400 fuel, where the most one call of it used is 399 (rule 18)" + }, + { + "file": "jobs/crafted-wasmq-request-member-added.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ request with a member the profile does not write (rule 2)" + }, + { + "file": "jobs/crafted-wasmq-contract-member-added.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract with a member section 2.2 does not list (rule 3)" + }, + { + "file": "jobs/crafted-wasmq-contract-profile.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract of another profile (rule 4)" + }, + { + "file": "jobs/crafted-wasmq-program-empty.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract whose program is 0 bytes long (rule 5)" + }, + { + "file": "jobs/crafted-wasmq-module-empty.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract whose module is 0 bytes long (rule 6)" + }, + { + "file": "jobs/crafted-wasmq-callouts-twice.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ contract with two callouts of one extern (rule 7)" + }, + { + "file": "jobs/crafted-wasmq-result-seed.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ result whose seed is not its contract's (rule 11)" + }, + { + "file": "jobs/crafted-wasmq-call-past-per-call.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ run whose largest call used 21751 fuel under a budget of 21750 a call (rule 14)" + }, + { + "file": "jobs/crafted-wasmq-calls-of-no-fuel.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "32 calls that returned having used no fuel, where entering a function weighs 1 (rule 20)" + }, + { + "file": "jobs/crafted-wasmq-priors-unpassed.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "32 shots of instances passed a prior of 87 bytes in less fuel than its bytes and its two calls take, cabi_realloc 2 at least and wasmq-prior 1 (rule 21)" + }, + { + "file": "jobs/crafted-wasmq-call-fault-other-module.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a call of a callout in the first module, whose fault names the second (rule 22)" + }, + { + "file": "jobs/crafted-wasmq-prior-fault-no-prior.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a fault while a prior is passed, in a module with no prior (rule 23)" + }, + { + "file": "jobs/crafted-wasmq-bad-result-in-init.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a bad_result from an init, which takes no result (rule 24)" + }, + { + "file": "jobs/crafted-wasmq-lifecycle-fuel-past-instances.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "an init fault in shot 1 that used 2 fuel, where the instances used 1, shot 0's init 1 of it at least (rule 25)" + }, + { + "file": "jobs/crafted-wasmq-job-state-later-instance.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a run of one instance for the run whose instance fails in shot 2 (rule 26)" + }, + { + "file": "jobs/crafted-wasmq-first-instance-after-call.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "the first shot's instance failing after a call was made, where no shot ran its steps (rules 27 and 32)" + }, + { + "file": "jobs/crafted-wasmq-fuel-fault-under-limit.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a call's fuel fault after 10 fuel, under its limit of 400 (rule 28)" + }, + { + "file": "jobs/crafted-wasmq-run-budget-not-spent.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a fault of the run's budget with 50 of its 60 used (rule 28)" + }, + { + "file": "jobs/crafted-wasmq-results-past-bits.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "2^20 shots of 257 bits, past the 2^28 bits a run's results hold (rule 29)" + }, + { + "file": "jobs/crafted-wasmq-work-past-bound.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a run given 2^39 fuel beside a module's code, past the 2^42 operations a run asks (rule 30)" + }, + { + "file": "jobs/crafted-wasmq-frames-past-envelope.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a prior of 2^27 bytes beside a program and a module, more than an envelope holds (rule 31)" + }, + { + "file": "jobs/crafted-wasmq-fault-null.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "an ok result with a fault member whose value is null: a member is present whatever its value (rule 18)" + }, + { + "file": "jobs/crafted-wasmq-no-qubit.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a wasmQ result of a program of no qubit, which no program a reader reads is (rule 12)" + }, + { + "file": "jobs/crafted-wasmq-calls-under-largest.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "32 calls in 21751 fuel, where the most one of them used is 21751 and each other used 1 at least (rule 20)" + }, + { + "file": "jobs/crafted-wasmq-calls-past-steps.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "32 shots that made 2^21 calls each and one more, where a shot runs at most 2^21 steps (rule 32)" + }, + { + "file": "jobs/crafted-wasmq-prior-past-instance-budget.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a prior of 87 bytes passed whole in one stretch, which takes 90 at least, under a lifecycle call's budget of 89 (rule 33)" + }, + { + "file": "jobs/crafted-wasmq-trap-of-no-fuel.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "an init that trapped after no fuel, where its entry is charged before it runs (rule 34)" + }, + { + "file": "jobs/crafted-wasmq-transcript-of-no-call.json", + "reasons": [ + "wasmq_form" + ], + "made_by": "crafted", + "what": "a fault before any call whose transcript is not the run's end alone (rule 35)" + }, + { + "file": "jobs/crafted-wasmq-respelt.json", + "reasons": [], + "made_by": "crafted", + "what": "a wasmQ job with its shots written 32.0 and 3.2e1 and a member of its contract named with an escape: the same sealed content" + }, + { + "file": "jobs/crafted-wasmq-envelope-changed.json", + "reasons": [], + "made_by": "crafted", + "what": "a wasmQ fault whose unsealed body says done, another method and an id: a note beside the verdict, not a reason" } ] } diff --git a/wai/quantum-conformance/tools/receipt_differential.py b/wai/quantum-conformance/tools/receipt_differential.py index cb46ebd0e..2bfbb18ee 100644 --- a/wai/quantum-conformance/tools/receipt_differential.py +++ b/wai/quantum-conformance/tools/receipt_differential.py @@ -27,14 +27,23 @@ of the reference, and changes nothing. (`receipt_verify.py --plant`), and succeeds only when the differential catches it: the proof that the check can fail. +`--second FILE` holds another second verifier (a mutant of receipt_verify.py) to the +same stages; `--wasmq-only` keeps to what holds a wasmQ job (its receipts, their +one-member changes, the rules stage and the labelled cases); `--first` stops at the +first stage that disagrees; and `RD_CACHE=DIR` keeps each verifier's answers, so that a +run over many mutants of one verifier asks the other once (`wasmq_mutants.py`). + Exit status 0 only when everything agrees (or, with --plant, when the plant is caught). Standard library only, Python 3.9+. """ import argparse +import base64 import copy +import hashlib import importlib.util import json import os +import re import subprocess import sys import tempfile @@ -43,12 +52,24 @@ from collections import Counter HERE = os.path.dirname(os.path.abspath(__file__)) CORPUS = os.path.join(HERE, "..", "receipts") SECOND = os.path.join(HERE, "receipt_verify.py") +ORIGINAL_SECOND = SECOND def compact(v): return json.dumps(v, ensure_ascii=False, separators=(",", ":")) +def spelt(v, raw): + """A line of v's text, each `raw` literal as it is written.""" + if isinstance(v, raw): + return v.literal + if isinstance(v, dict): + return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + spelt(x, raw) for k, x in v.items()) + "}" + if isinstance(v, list): + return "[" + ",".join(spelt(x, raw) for x in v) + "]" + return compact(v) + + def verdicts(lines_text, json_lines): out = [] for line in lines_text.splitlines(): @@ -60,29 +81,110 @@ def verdicts(lines_text, json_lines): return out -def run_reference_files(ref, files): - out = [] - for f in files: - p = subprocess.run([ref, "verify", "--json", f], capture_output=True, text=True) - if p.returncode not in (0, 1) or not p.stdout.strip(): - raise SystemExit("the reference failed on %s (status %d): %s" % (f, p.returncode, p.stderr.strip()[:400])) - out.append(json.loads(p.stdout.strip().splitlines()[-1])) +def _who(cmd): + """What decides a verifier's answers, for the cache: the reference binary (its path, size + and time) and the mutant it is asked to be, or the second verifier's text and its plant.""" + h = hashlib.sha256() + script = any(part.endswith(".py") for part in cmd) + for part in cmd: + h.update(part.encode() + b"\0") + if part.endswith(".py") and os.path.isfile(part): + with open(part, "rb") as f: + h.update(f.read()) + elif os.path.isfile(part) and os.access(part, os.X_OK): + st = os.stat(part) + h.update(("%d %d" % (st.st_size, st.st_mtime_ns)).encode()) + if not script: + h.update(os.environ.get("WQ_MUTANT", "").encode()) + return h.hexdigest() + + +def cached(cmd, inputs, compute): + """compute(), or what it gave before for the same verifier and the same inputs when + RD_CACHE names a directory: a run of many mutants of one verifier asks the other the + same questions each time.""" + d = os.environ.get("RD_CACHE") + if not d: + return compute() + h = hashlib.sha256(_who(cmd).encode()) + for b in inputs: + h.update(hashlib.sha256(b).digest()) + path = os.path.join(d, h.hexdigest() + ".json") + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError): + pass + out = compute() + os.makedirs(d, exist_ok=True) + with open(path + ".tmp%d" % os.getpid(), "w", encoding="utf-8") as f: + json.dump(out, f) + os.replace(path + ".tmp%d" % os.getpid(), path) return out +def _contents(files): + out = [] + for f in files: + with open(f, "rb") as h: + out.append(h.read()) + return out + + +def run_reference_files(ref, files): + def compute(): + out = [] + for f in files: + p = subprocess.run([ref, "verify", "--json", f], capture_output=True, text=True) + if p.returncode not in (0, 1) or not p.stdout.strip(): + raise SystemExit("the reference failed on %s (status %d): %s" % (f, p.returncode, p.stderr.strip()[:400])) + out.append(json.loads(p.stdout.strip().splitlines()[-1])) + return out + return cached([ref, "verify", "--json"], _contents(files), compute) + + def run_second_files(files, plant): - cmd = [sys.executable, "-I", SECOND] + (["--plant", plant] if plant else []) + files - p = subprocess.run(cmd, capture_output=True, text=True) - if p.returncode not in (0, 1): - raise SystemExit("the second verifier failed (status %d): %s" % (p.returncode, p.stderr.strip()[:400])) - return verdicts(p.stdout, len(files)) + cmd = [sys.executable, "-I", SECOND] + (["--plant", plant] if plant else []) + + def compute(): + p = subprocess.run(cmd + files, capture_output=True, text=True) + if p.returncode not in (0, 1): + raise SystemExit("the second verifier failed (status %d): %s" % (p.returncode, p.stderr.strip()[:400])) + return verdicts(p.stdout, len(files)) + return cached(cmd, _contents(files), compute) def run_lines(cmd, path, n): - p = subprocess.run(cmd + [path], capture_output=True, text=True) - if p.returncode not in (0, 1): - raise SystemExit("%s failed (status %d): %s" % (cmd[0], p.returncode, p.stderr.strip()[:400])) - return verdicts(p.stdout, n) + def compute(): + p = subprocess.run(cmd + [path], capture_output=True, text=True) + if p.returncode not in (0, 1): + raise SystemExit("%s failed (status %d): %s" % (cmd[0], p.returncode, p.stderr.strip()[:400])) + return verdicts(p.stdout, n) + return cached(cmd, _contents([path]), compute) + + +def rules_of(v): + """The numbers of the rules of a wasmQ job's form a verdict states as broken (each + problem names its rule first), or None when it states none or only the first of many.""" + w = v.get("wasmq") + if not isinstance(w, dict) or not isinstance(w.get("problems"), list): + return None + out = set() + for p in w["problems"]: + if not isinstance(p, str) or p.startswith("and "): + return None + m = re.match(r"rule (\d+): ", p) + if not m: + return None + out.add(int(m.group(1))) + return out + + +def same_rules(what, x, y, problems): + """Both verifiers state the same rules broken, where both state them in full.""" + rx, ry = rules_of(x), rules_of(y) + if rx is not None and ry is not None and rx != ry: + problems.append("%s: the reference states rules %s broken; the second verifier %s" % (what, sorted(rx), sorted(ry))) def said(v): @@ -217,6 +319,461 @@ def edges(body, seal): return out +# ------------------------------------------------- the rules of a wasmQ job's form + +def rule_cases(corpus, seal): + """(what, body, expected reasons): for each rule of quantum-wasmq section 6.5, the + genuine wasmQ jobs of the corpus changed at the rule's bound, on both sides of it where + it has two, and sealed again with the demo key, so that the rule alone decides. Each + expected verdict is written here by hand from the text, as the manifest's are: none is + copied from a verifier. A verifier with a rule dropped, or a bound moved, gives some + case here another verdict.""" + def load(name): + with open(os.path.join(corpus, "jobs", name), encoding="utf-8") as f: + return json.load(f) + run, fault, init, job, frun, prior = (load("cli-wasmq-%s.json" % n) for n in ("run", "fault", "fault-init", "state-job", "fault-run", "fault-prior")) + out = [] + g = generator() + + def case(rule, what, base, edit, valid, respell=None): + b = copy.deepcopy(base) + edit(b) + # With no call returned, the transcript the writer gives the result as changed + # (rule 35), unless the change was to the transcript. + sealed = seal(g.retranscribed(base, b)) + if respell: + # After the seal: another spelling of a number, not another value, so it holds. + respell(sealed) + sealed = spelt(sealed, g.Raw) + out.append(("rule %s, %s" % (rule, what), sealed, [] if valid else ["wasmq_form"])) + + def contract(b): + return b["request"]["wasmq"] + + def budget(name, value): + def f(b): + contract(b)["run"]["fuel"][name] = value + b["result"]["fuel"]["budget"][name] = value + return f + + def stretches(b): + return sum(2 + len(m["priors"]) for m in contract(b)["modules"]) + + def second_module(bytes0, bytes1, named=True): + def f(b): + c = contract(b) + c["modules"][0]["bytes"] = bytes0 + c["modules"].append({"name": "second", "blake3": "cd" * 32, "bytes": bytes1, "priors": []}) + b["result"]["modules"].append({"name": "second", "blake3": "cd" * 32}) + if named: + c["callouts"].append({"extern": "g", "module": 1, "export": "g"}) + return f + + def each(*edits): + def f(b): + for e in edits: + e(b) + return f + + def at(path, value): + def f(b): + o = b + for k in path[:-1]: + o = o[k] + o[path[-1]] = value + return f + + def calls(b): + (name, e), = b["result"]["fuel"]["callouts"].items() + return e + + def passed(b): + # What passing every prior to one shot's instances takes at the least (rule 21): + # cabi_realloc, 2 at least, its bytes, and wasmq-prior, 1 at least. + return sum(p["bytes"] + 3 for m in contract(b)["modules"] for p in m["priors"]) + + def shots(n): + def f(b): + width = len(next(iter(b["result"]["counts"]))) + contract(b)["run"]["shots"] = b["result"]["shots"] = b["result"]["shots_run"] = n + b["result"]["counts"] = {"1" * width: n} + fuel = b["result"]["fuel"] + more = max(0, n * passed(b) - fuel["instances"]) + fuel.update(instances=fuel["instances"] + more, used=fuel["used"] + more) + return f + + def later_shot(shot, state=None, instances=1, fuel=1, calls=None): + # The init fault moved to `shot`, after that many shots of one call each. + def f(b): + n = shot if calls is None else calls + if state: + contract(b)["run"]["state"] = state + b["result"].update(shots_run=shot, counts={"0": shot} if shot else {}, callouts=n) + b["result"]["fuel"].update(instances=instances, callouts={"f": {"calls": n, "fuel": n, "max": 1}} if n else {}, used=instances + n) + b["result"]["fault"].update(shot=shot, fuel=fuel) + return f + + def fault_module(name): + def f(b): + second_module(57, 57)(b) + b["result"]["fault"]["module"] = name + return f + + def counts(width, n): + return lambda b: b["result"].update(counts={"0" * width: n}) + + def work_left(b): + # The most per_job the rest of rule 30's work leaves room for. + c, r = contract(b), b["result"] + n, s = r["qubits"], c["run"]["shots"] + inst = 1 if c["run"]["state"] == "job" else s + rest = s * 2 * n * max(1, -(-n // 32)) + inst * sum(4096 * (1 + 2 * len(m["priors"])) + m["bytes"] for m in c["modules"]) + 131072 * sum(m["bytes"] for m in c["modules"]) + 4097 * r["callouts"] + return (2 ** 42 - rest) // 8 + + def frames(b): + c = contract(b) + return c["program"]["bytes"] + sum(m["bytes"] + sum(p["bytes"] for p in m["priors"]) for m in c["modules"]) + + def most_prior(b): + # The most bytes the first prior may have: 2^27 less the envelope's other bytes, its + # magic and lengths, its manifest, its contract and its other frames (rule 31). + c, r = contract(b), b["result"] + c["modules"][0]["priors"][0]["bytes"] = 10 ** 8 + manifest = 192 + sum(len(str(x)) for x in (r["qubits"], c["run"]["shots"], c["run"]["seed"])) + rest = 20 + manifest + len(g.text(c).encode("utf-8")) + frames(b) - 10 ** 8 + return 2 ** 27 - rest + + def calls_of(n, max_fuel=1): + # n calls of the run's one callout, each of 1 but the most. + def f(b): + b["result"]["callouts"] = n + calls(b).update(calls=n, fuel=n - 1 + max_fuel, max=max_fuel) + b["result"]["fuel"]["used"] = b["result"]["fuel"]["instances"] + calls(b)["fuel"] + return f + + def own_prior(nbytes): + # The first module given a prior of nbytes. + return lambda b: contract(b)["modules"][0].update(priors=[{"name": "p", "blake3": "ef" * 32, "bytes": nbytes}]) + + def fault_of(reason, fuel): + # The call's fault of `fault`, stopped for reason after fuel. + def f(b): + b["result"]["fault"].update(reason=reason, fuel=fuel) + b["result"]["fault"].pop("budget", None) + calls(b).update(calls=1, fuel=fuel, max=fuel) + b["result"]["fuel"]["used"] = fuel + return f + + def own_fuel(delta): + def f(b): + e = calls(b) + e["fuel"] = e["max"] + delta + b["result"]["fuel"]["used"] = b["result"]["fuel"]["instances"] + e["fuel"] + return f + + R, F = ("result",), ("result", "fuel") + # 1: no number past 2^53 - 1 (it breaks another rule too: each place a number may be holds it) + case(1, "an input of 2^53", run, lambda b: contract(b)["program"].update(inputs={"n": 2 ** 53}), False) + # 2: the request + case(2, "a member the request does not define", run, at(("request", "x"), 1), False) + case(2, "envelope_blake3 not a digest", run, lambda b: b["request"].update(envelope_blake3=b["request"]["envelope_blake3"].upper()), False) + # 3, 4: the contract's members and profile + case(3, "a member the contract does not define", run, lambda b: contract(b).update(x=1), False) + case(3, "a member the run does not define", run, lambda b: contract(b)["run"].update(x=1), False) + case(4, "another profile", run, lambda b: contract(b).update(profile="wai.quantum.wasmq/2"), False) + # 5: the program + case(5, "a program of 2^20 bytes", run, lambda b: contract(b)["program"].update(bytes=2 ** 20), True) + case(5, "a program of 2^20 + 1 bytes", run, lambda b: contract(b)["program"].update(bytes=2 ** 20 + 1), False) + case(5, "a program of 0 bytes", run, lambda b: contract(b)["program"].update(bytes=0), False) + case(5, "a program of 1 byte", run, lambda b: contract(b)["program"].update(bytes=1), True) + case(5, "an input of 2^53 - 1", run, lambda b: contract(b)["program"].update(inputs={"n": 2 ** 53 - 1}), True) + case(5, "an input of -(2^53 - 1)", run, lambda b: contract(b)["program"].update(inputs={"n": -(2 ** 53 - 1)}), True) + case(5, "an input of 0.5", run, lambda b: contract(b)["program"].update(inputs={"n": 0.5}), False) + # 6: the modules and their priors + case(6, "a module of 1 byte", run, lambda b: contract(b)["modules"][0].update(bytes=1), True) + case(6, "a module of 0 bytes", run, lambda b: contract(b)["modules"][0].update(bytes=0), False) + case(6, "a prior of 0 bytes", run, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=0), True) + case(6, "a prior of 0.5 bytes", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=0.5), False) + case(6, "a module of 2^19 bytes", run, lambda b: contract(b)["modules"][0].update(bytes=2 ** 19), True) + case(6, "a module of 2^19 + 1 bytes", run, lambda b: contract(b)["modules"][0].update(bytes=2 ** 19 + 1), False) + case(6, "a module's priors null", run, lambda b: contract(b)["modules"][0].update(priors=None), False) + case(6, "a prior's name null", run, lambda b: contract(b)["modules"][0]["priors"][0].update(name=None), False) + case(6, "a module's name null, alike in the result", run, lambda b: (contract(b)["modules"][0].update(name=None), b["result"]["modules"][0].update(name=None)), False) + case(6, "modules of 2^19 bytes together", run, second_module(2 ** 18, 2 ** 18), True) + case(6, "modules of 2^19 + 1 bytes together", run, second_module(2 ** 18, 2 ** 18 + 1), False) + # 7: the callouts + case(7, "a module no callout names", run, second_module(39442, 1, named=False), False) + case(7, "two callouts of one extern", run, lambda b: contract(b)["callouts"].append({"extern": "decode64", "module": 0, "export": "x"}), False) + case(7, "a callout of a module the contract does not hold", run, lambda b: contract(b)["callouts"][0].update(module=1), False) + case(7, "a callout of the module past the last, beside one of each module", run, lambda b: contract(b)["callouts"].append({"extern": "g", "module": 1, "export": "g"}), False) + case(7, "a callout of the last module, beside one of each module", run, second_module(39442, 57), True) + # No module and no callout: rule 7's first clause alone holds it, its others vacuous. + def nothing(b): + contract(b).update(modules=[], callouts=[]) + b["result"].update(modules=[], callouts=0) + b["result"]["fuel"].update(callouts={}, instances=0, used=0) + case(7, "no module and no callout", run, nothing, False) + case(7, "no module, null for the modules, and no callout", run, each(nothing, lambda b: contract(b).update(modules=None)), False) + # 8: the run + case(8, "2^20 shots", run, shots(2 ** 20), True) + case(8, "2^20 + 1 shots", run, shots(2 ** 20 + 1), False) + for name in ("per_call", "per_instance", "per_job"): + # per_job at 2^39 is past rule 30's work beside any module: it is held there. + case(30 if name == "per_job" else 8, "the budget %s at 2^39" % name, run, budget(name, 2 ** 39), name != "per_job") + case(30 if name == "per_job" else 8, "the budget %s at 2^39 + 1" % name, run, budget(name, 2 ** 39 + 1), False) + case(8, "a seed of 0, alike in the result", run, lambda b: (contract(b)["run"].update(seed=0), b["result"].update(seed=0)), True) + case(8, "a seed below 0, alike in the result", run, lambda b: (contract(b)["run"].update(seed=-1), b["result"].update(seed=-1)), False) + case(8, "a seed of 2^53 - 1, alike in the result", run, lambda b: (contract(b)["run"].update(seed=2 ** 53 - 1), b["result"].update(seed=2 ** 53 - 1)), True) + case(8, "1 shot", run, shots(1), True) + case(8, "0 shots", job, lambda b: (contract(b)["run"].update(shots=0), b["result"].update(shots=0, shots_run=0, counts={}, callouts=0), b["result"]["fuel"].update(callouts={}, used=b["result"]["fuel"]["instances"])), False) + case(8, "a budget of 0", fault, budget("per_instance", 0), True) + case(8, "a call's budget of 0, its one call stopped at once", fault, each(budget("per_call", 0), lambda b: (calls(b).update(fuel=0, max=0), b["result"]["fuel"].update(used=0), b["result"]["fault"].update(fuel=0))), True) + case(8, "another state", run, at(("request", "wasmq", "run", "state"), "call"), False) + # 9, 10: the result's members, and the form of its seal + case(9, "a member the result does not define", run, at(R + ("x",), 1), False) + first = copy.deepcopy(run) + out.append(("rule 10, sealed in the first form", seal(first, form="first"), ["wasmq_form"])) + # 11: what the result repeats + case(11, "another seed in the result", run, lambda b: b["result"].update(seed=b["result"]["seed"] + 1), False) + case(11, "another shots in the result", run, lambda b: b["result"].update(shots=b["result"]["shots"] + 1), False) + case(11, "another accuracy", run, at(R + ("accuracy",), {"class": "exact", "x": 1}), False) + case(11, "another energy", run, at(R + ("energy",), {"unmetered": "a meter"}), False) + # 12: qubits + case(12, "2^15 qubits", run, at(R + ("qubits",), 2 ** 15), True) + case(12, "1 qubit", run, at(R + ("qubits",), 1), True) + case(12, "0 qubits", run, at(R + ("qubits",), 0), False) + case(12, "qubits null", run, at(R + ("qubits",), None), False) + case(12, "2^15 + 1 qubits", run, at(R + ("qubits",), 2 ** 15 + 1), False) + # 13: the fuel's budget and what it used + case(13, "used equal to per_job", run, lambda b: budget("per_job", b["result"]["fuel"]["used"])(b), True) + case(13, "used past per_job", run, lambda b: budget("per_job", b["result"]["fuel"]["used"] - 1)(b), False) + case(13, "a budget the contract does not give", run, lambda b: b["result"]["fuel"]["budget"].update(per_call=b["result"]["fuel"]["budget"]["per_call"] + 1), False) + # 14: each callout's fuel + case(14, "a call's largest equal to per_call", run, lambda b: budget("per_call", calls(b)["max"])(b), True) + case(14, "a call's largest past per_call", run, lambda b: budget("per_call", calls(b)["max"] - 1)(b), False) + case(14, "a call's largest equal to its fuel", fault, own_fuel(0), True) + case(14, "a call's largest past its fuel", run, own_fuel(-1), False) + case(14, "a callout with no call", run, lambda b: (calls(b).update(calls=0, fuel=0, max=0), b["result"].update(callouts=0), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"])), False) + case(20, "fuel past its calls of their largest", run, lambda b: (calls(b).update(fuel=calls(b)["calls"] * calls(b)["max"] + 1), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"] + calls(b)["fuel"])), False) + # 15: what adds up + case(15, "a call more than the fuel counts", run, lambda b: b["result"].update(callouts=b["result"]["callouts"] + 1), False) + # 16: the result's modules + case(16, "a module of the result named otherwise", run, lambda b: b["result"]["modules"][0].update(name="other"), False) + # 17: what the instances could be given + case(17, "a fault's instance counted: one, given 1 each stretch", init, budget("per_instance", 1), True) + case(17, "one instance for the run, at its bound", job, lambda b: budget("per_instance", -(-b["result"]["fuel"]["instances"] // stretches(b)))(b), True) + case(17, "one instance for the run, past its bound", job, lambda b: budget("per_instance", (b["result"]["fuel"]["instances"] - 1) // stretches(b))(b), False) + # 18: how it ended + case(18, "a fault in init within its phase's budget", init, each(budget("per_instance", 2), at(F + ("instances",), 2), at(F + ("used",), 2), at(R + ("fault", "fuel"), 2)), True) + case(18, "a fault in init past its phase's budget", init, each(budget("per_instance", 1), at(F + ("instances",), 2), at(F + ("used",), 2), at(R + ("fault", "fuel"), 2)), False) + case(18, "a fault after every shot", fault, lambda b: (b["result"].update(shots_run=32, counts={"0": 32}), b["result"]["fault"].update(shot=32)), False) + case(18, "a fault in another shot than the next", fault, at(R + ("fault", "shot"), 1), False) + case(28, "a call's fuel fault of the run's budget, with the run's budget not spent", fault, at(R + ("fault", "budget"), "run"), False) + case(18, "a call's fuel fault of the instance's budget", fault, at(R + ("fault", "budget"), "instance"), False) + case(18, "a fault in init that names a callout", init, at(R + ("fault", "callout"), "f"), False) + case(18, "a call's fault the fuel counts no call of", fault, lambda b: (b["result"].update(callouts=0), b["result"]["fuel"].update(callouts={}, instances=400)), False) + case(18, "a call that stopped at its callout's largest", fault, lambda b: (b["result"].update(callouts=2), b["result"]["fuel"]["callouts"]["f"].update(calls=2, fuel=401, max=400), b["result"]["fuel"].update(used=401)), True) + case(18, "a call that stopped past its callout's largest", fault, lambda b: (b["result"].update(callouts=2), b["result"]["fuel"]["callouts"]["f"].update(calls=2, fuel=401, max=399), b["result"]["fuel"].update(used=401)), False) + case(18, "a lifecycle fault whose callout is null", init, at(R + ("fault", "callout"), None), False) + case(18, "a trap whose budget is null", init, at(R + ("fault", "budget"), None), False) + case(18, "a trap in seed", init, at(R + ("fault", "phase"), "seed"), True) + case(18, "a fault in another phase", init, at(R + ("fault", "phase"), "start"), False) + case(18, "a trap of another reason the profile names", init, at(R + ("fault", "reason"), "invalid_conversion_to_integer"), True) + case(18, "a fault in a shot after shots_run", frun, at(R + ("fault", "shot"), 1), False) + case(18, "a fault in a module named null", init, at(R + ("fault", "module"), None), False) + case(18, "a trap that names a budget", init, at(R + ("fault", "budget"), "instance"), False) + case(18, "an ok result whose fault is null", run, at(R + ("fault",), None), False) + case(18, "a call's fault whose callout is null", fault, at(R + ("fault", "callout"), None), False) + case(18, "a fuel fault whose budget is null", fault, at(R + ("fault", "budget"), None), False) + # 19: the counts + case(19, "outcomes of two widths", run, lambda b: (b["result"]["counts"].update({next(iter(b["result"]["counts"])): b["result"]["counts"][next(iter(b["result"]["counts"]))] - 1}), b["result"]["counts"].update({"0": 1})), False) + case(19, "a count of 0", run, lambda b: b["result"]["counts"].update({"0" * len(next(iter(b["result"]["counts"]))): 0}), False) + case(19, "an outcome of no bits", run, counts(0, 32), True) + case(19, "an outcome holding a 2", run, lambda b: b["result"].update(counts={"012": 32}), False) + case(19, "counts null", fault, at(R + ("counts",), None), False) + # Round 4's labelled cases, each where a weakening of one rule moves a verdict. + case(2, "envelope_blake3 of 65 digits", run, lambda b: b["request"].update(envelope_blake3=b["request"]["envelope_blake3"] + "0"), False) + case(12, "transcript_blake3 of 63 digits", run, lambda b: b["result"].update(transcript_blake3=b["result"]["transcript_blake3"][:63]), False) + case(3, "run's fuel a member more, the budget alike", run, budget("z", 1), False) + case(3, "the program a member more", run, lambda b: contract(b)["program"].update(z=1), False) + case(4, "a profile one letter longer", run, lambda b: contract(b).update(profile="wai.quantum.wasmq/1x"), False) + case(5, "inputs null", run, lambda b: contract(b)["program"].update(inputs=None), False) + case(5, "the program's bytes 1104.5", run, lambda b: contract(b)["program"].update(bytes=1104.5), False) + case(5, "an input that is true", run, lambda b: contract(b)["program"].update(inputs={"n": True}), False) + case(6, "a module's bytes 39442.5", run, lambda b: contract(b)["modules"][0].update(bytes=39442.5), False) + case(7, "an export null", run, lambda b: contract(b)["callouts"][0].update(export=None), False) + case(7, "an extern null, with no call made", init, lambda b: contract(b)["callouts"][0].update(extern=None), False) + case(7, "a callout of module 0.5", run, lambda b: contract(b)["callouts"].append({"extern": "g", "module": 0.5, "export": "g"}), False) + + def respell(path, text, value): + def f(s): + o = s + for k in path[:-1]: + o = o[k] + o[path[-1]] = g.Raw(text, value) + return f + case(11, "the result's seed written 1234.0", run, lambda b: None, True, respell(R + ("seed",), "1234.0", run["result"]["seed"])) + case(12, "qubits written 17.0", run, lambda b: None, True, respell(R + ("qubits",), "17.0", 17)) + case(13, "a budget written 1e7", run, lambda b: None, True, respell(F + ("budget", "per_call"), "1e7", 10000000)) + case(14, "the callouts' fuel null", init, at(F + ("callouts",), None), False) + case(14, "the callouts' fuel an array", init, at(F + ("callouts",), []), False) + case(15, "a call fewer than the fuel counts", run, lambda b: b["result"].update(callouts=b["result"]["callouts"] - 1), False) + case(16, "the result's modules in another order", run, lambda b: (second_module(39442, 1)(b), b["result"].update(modules=b["result"]["modules"][::-1])), False) + case(17, "the instances' fuel past 32 x S x per_instance", run, budget("per_instance", 2583), False) + case(17, "one instance for the run, a fault, past 1 x S x per_instance", init, each(at(("request", "wasmq", "run", "state"), "job"), budget("per_instance", 1), at(F + ("instances",), 3), at(F + ("used",), 3)), False) + case(25, "an init fault's fuel past what was used", init, at(R + ("fault", "fuel"), 2), False) + case(18, "a call's fault past its callout's max", frun, at(R + ("fault", "fuel"), 24), False) + case(18, "an ok result one shot past its shots", run, lambda b: (b["result"]["counts"].update({next(iter(b["result"]["counts"])): b["result"]["counts"][next(iter(b["result"]["counts"]))] + 1}), b["result"].update(shots_run=33)), False) + # 20: the fuel of the calls that returned, each 1 to the most one used + case(20, "calls that returned using 1 each", run, lambda b: (calls(b).update(fuel=32, max=1), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"] + 32)), True) + case(20, "calls that returned using nothing", run, lambda b: (calls(b).update(fuel=0, max=0), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"])), False) + case(20, "two calls that returned and one that stopped, in 6", frun, each(budget("per_job", 6), lambda b: (calls(b).update(fuel=6, max=4), b["result"]["fuel"].update(used=6))), True) + case(20, "two calls that returned and one that stopped, in 5", frun, each(budget("per_job", 5), lambda b: (calls(b).update(fuel=5, max=4), b["result"]["fuel"].update(used=5))), False) + case(20, "calls that returned at the most one used", frun, lambda b: calls(b).update(max=23), True) + case(20, "calls that returned past the most one used", frun, lambda b: calls(b).update(max=21), False) + case(20, "the one call, the one that stopped, its fuel more than the fault's", fault, lambda b: (b["result"]["fault"].update(reason="unreachable", fuel=10), b["result"]["fault"].pop("budget")), False) + case(20, "the one call, the one that stopped, its fuel the fault's", fault, lambda b: (b["result"]["fault"].update(reason="unreachable", fuel=10), b["result"]["fault"].pop("budget"), calls(b).update(fuel=10, max=10), b["result"]["fuel"].update(used=10)), True) + # 21: the priors passed to each shot's instances + case(21, "instances that passed each prior at the least", run, lambda b: b["result"]["fuel"].update(instances=32 * passed(b), used=32 * passed(b) + calls(b)["fuel"]), True) + case(21, "instances that passed the priors in less", run, lambda b: b["result"]["fuel"].update(instances=32 * passed(b) - 1, used=32 * passed(b) - 1 + calls(b)["fuel"]), False) + case(21, "one instance for the run, its prior in less", job, lambda b: b["result"]["fuel"].update(instances=passed(b) - 1, used=passed(b) - 1 + calls(b)["fuel"]), False) + case(21, "a prior's fault: no prior passed whole", prior, lambda b: None, True) + # 22: a call's fault in its callout's module + case(22, "a call's fault in its callout's module, beside another", fault, fault_module("m"), True) + case(22, "a call's fault in the other module", fault, fault_module("second"), False) + # 23, 24: a phase against the module's priors, a reason against its phase + case(23, "a prior's fault in a module with none", init, at(R + ("fault", "phase"), "prior"), False) + case(24, "a bad_result while a prior is passed", prior, at(R + ("fault", "reason"), "bad_result"), True) + case(24, "a bad_result in a call", fault, each(at(R + ("fault", "reason"), "bad_result"), lambda b: b["result"]["fault"].pop("budget")), True) + case(24, "a bad_result in init", init, at(R + ("fault", "reason"), "bad_result"), False) + case(24, "a bad_result in seed", init, each(at(R + ("fault", "reason"), "bad_result"), at(R + ("fault", "phase"), "seed")), False) + # 25, 26, 27: a lifecycle fault against the instances, the state and the calls + case(25, "an init fault in shot 1 within the instances' fuel", init, later_shot(1, instances=3, fuel=2), True) + case(25, "an init fault in shot 1, shot 0's init uncounted", init, later_shot(1, instances=2, fuel=2), False) + case(25, "an init fault in shot 1 past the instances' fuel", init, later_shot(1, instances=1, fuel=2), False) + case(26, "one instance for the run, failing in shot 0", init, at(("request", "wasmq", "run", "state"), "job"), True) + case(26, "one instance for the run, failing in shot 2", init, later_shot(2, state="job"), False) + case(26, "a fresh instance each shot, failing in shot 2", init, later_shot(2, instances=3), True) + case(27, "the first shot's instances failing after a call", init, later_shot(0, calls=1), False) + # 28: a fuel fault used its whole limit + case(28, "a call's fuel fault at its limit", fault, each(budget("per_call", 10), lambda b: (calls(b).update(fuel=10, max=10), b["result"]["fuel"].update(used=10), b["result"]["fault"].update(fuel=10))), True) + case(28, "a call's fuel fault under its limit", fault, lambda b: (calls(b).update(fuel=10, max=10), b["result"]["fuel"].update(used=10), b["result"]["fault"].update(fuel=10)), False) + case(28, "an instance's fuel fault at its limit", init, each(budget("per_instance", 1), at(R + ("fault", "reason"), "fuel_exhausted"), at(R + ("fault", "budget"), "instance")), True) + case(28, "an instance's fuel fault under its limit", init, each(at(R + ("fault", "reason"), "fuel_exhausted"), at(R + ("fault", "budget"), "instance")), False) + case(28, "the run's budget run out, not all used", frun, budget("per_job", 51), False) + case(28, "the run's budget the limit, under the call's own", frun, each(budget("per_call", 24), at(R + ("fault", "fuel"), 23), lambda b: calls(b).update(max=23)), True) + case(28, "the run's budget the limit, at the call's own", frun, each(budget("per_call", 23), at(R + ("fault", "fuel"), 23)), False) + case(28, "an instance's run budget, at the instance's own", init, each(budget("per_job", 1), budget("per_instance", 1), at(R + ("fault", "reason"), "fuel_exhausted"), at(R + ("fault", "budget"), "run")), False) + case(28, "an instance's run budget, under the instance's own", init, each(budget("per_job", 1), budget("per_instance", 2), at(R + ("fault", "reason"), "fuel_exhausted"), at(R + ("fault", "budget"), "run")), True) + # 29: the bits of a shot, and of the shots + case(29, "outcomes of 2^20 bits", run, counts(2 ** 20, 32), True) + case(29, "outcomes of 2^20 + 1 bits", run, counts(2 ** 20 + 1, 32), False) + case(29, "2^20 shots of 256 bits", frun, each(lambda b: (contract(b)["run"].update(shots=2 ** 20), b["result"].update(shots=2 ** 20)), counts(256, 2)), True) + case(29, "2^20 shots of 257 bits", frun, each(lambda b: (contract(b)["run"].update(shots=2 ** 20), b["result"].update(shots=2 ** 20)), counts(257, 2)), False) + # 30: the work a receipt shows + case(30, "per_job at the most the rest leaves", run, lambda b: budget("per_job", work_left(b))(b), True) + case(30, "per_job one past it", run, lambda b: budget("per_job", work_left(b) + 1)(b), False) + case(30, "2^15 qubits over 2^20 shots", frun, each(lambda b: (contract(b)["run"].update(shots=2 ** 20), b["result"].update(shots=2 ** 20, qubits=2 ** 15))), False) + case(30, "one instance for the run: its instance counted once", job, lambda b: budget("per_job", work_left(b))(b), True) + # 31: the envelope + case(31, "an envelope of 2^27 bytes", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=most_prior(b)), True) + case(31, "an envelope of 2^27 + 1 bytes", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=most_prior(b) + 1), False) + case(31, "frames one short of 2^27, the envelope past it", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 - 1 - (frames(b) - contract(b)["modules"][0]["priors"][0]["bytes"])), False) + # 20: the most one call used, a returned call's, and each other 1 at least + case(20, "32 calls, the most 21751, in 21751 + 31", run, calls_of(32, 21751), True) + case(20, "32 calls, the most 21751, in 21751 + 30", run, each(calls_of(32, 21751), lambda b: (calls(b).update(fuel=21751 + 30), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"] + 21751 + 30))), False) + case(20, "two returned, the most 23, beside a stopped call of 4, in 28", frun, each(budget("per_job", 28), lambda b: (calls(b).update(fuel=28, max=23), b["result"]["fuel"].update(used=28))), True) + case(20, "two returned, the most 23, beside a stopped call of 4, in 27", frun, each(budget("per_job", 27), lambda b: (calls(b).update(fuel=27, max=23), b["result"]["fuel"].update(used=27))), False) + case(20, "one returned in 5, then one stopped at 0, the run's budget spent", frun, each(budget("per_job", 5), lambda b: (b["result"].update(callouts=2), calls(b).update(calls=2, fuel=5, max=5), b["result"]["fuel"].update(used=5), b["result"]["fault"].update(fuel=0))), True) + case(20, "two returned in 11, the most 10, then one stopped at 0", frun, each(budget("per_job", 11), lambda b: (calls(b).update(fuel=11, max=10), b["result"]["fuel"].update(used=11), b["result"]["fault"].update(fuel=0))), True) + case(20, "two returned in 10, the most 10, then one stopped at 0", frun, each(budget("per_job", 10), lambda b: (calls(b).update(fuel=10, max=10), b["result"]["fuel"].update(used=10), b["result"]["fault"].update(fuel=0))), False) + # 21, 25: what a lifecycle fault shows of the set it stopped + case(25, "an init stopped after its 100-byte prior, in 104", init, each(own_prior(100), at(F + ("instances",), 104), at(F + ("used",), 104)), True) + case(25, "an init stopped after its 100-byte prior, in 103", init, each(own_prior(100), at(F + ("instances",), 103), at(F + ("used",), 103)), False) + case(21, "an init stopped after its 100-byte prior, in 102", init, each(own_prior(100), at(F + ("instances",), 102), at(F + ("used",), 102)), False) + case(25, "a seed stopped after its 100-byte prior's module, passed none", init, each(own_prior(100), at(R + ("fault", "phase"), "seed")), True) + case(25, "the second module's init stopped after the first's prior, in 104", init, each(second_module(50, 57), own_prior(100), at(R + ("fault", "module"), "second"), at(F + ("instances",), 104), at(F + ("used",), 104)), True) + case(25, "the second module's init stopped after the first's prior, in 103", init, each(second_module(50, 57), own_prior(100), at(R + ("fault", "module"), "second"), at(F + ("instances",), 103), at(F + ("used",), 103)), False) + case(25, "a seed stopped in shot 1, shot 0's seed shown", init, each(later_shot(1, instances=2, fuel=1), at(R + ("fault", "phase"), "seed")), True) + case(25, "a seed stopped in shot 1, shot 0's seed uncounted", init, each(later_shot(1, instances=1, fuel=1), at(R + ("fault", "phase"), "seed")), False) + # 29: the longest name, in characters + case(29, "two widths, the longer past 2^20", run, lambda b: b["result"].update(counts={next(iter(b["result"]["counts"])): 31, "0" * (2 ** 20 + 1): 1}), False) + case(19, "2^20 shots of a name of 256 characters in 257 bytes", frun, each(lambda b: (contract(b)["run"].update(shots=2 ** 20), b["result"].update(shots=2 ** 20)), lambda b: b["result"].update(counts={"\u00e9" + "0" * 255: 2})), False) + case(29, "2^20 shots of a name of 257 characters", frun, each(lambda b: (contract(b)["run"].update(shots=2 ** 20), b["result"].update(shots=2 ** 20)), lambda b: b["result"].update(counts={"\u00e9" + "0" * 256: 2})), False) + # 32: the calls a shot can make + case(32, "32 shots of 2^21 calls", run, calls_of(32 * 2 ** 21), True) + case(32, "32 shots of 2^21 calls and one more", run, calls_of(32 * 2 ** 21 + 1), False) + case(32, "a call's fault in the first shot, after 2^21 - 1 calls", fault, lambda b: (b["result"].update(callouts=2 ** 21), calls(b).update(calls=2 ** 21, fuel=2 ** 21 - 1 + 400), b["result"]["fuel"].update(used=2 ** 21 - 1 + 400)), True) + case(32, "a call's fault in the first shot, after 2^21 calls", fault, lambda b: (b["result"].update(callouts=2 ** 21 + 1), calls(b).update(calls=2 ** 21 + 1, fuel=2 ** 21 + 400), b["result"]["fuel"].update(used=2 ** 21 + 400)), False) + # 33: a prior passed whole within one lifecycle call's limit + case(33, "a prior of 87 bytes under 90 a lifecycle call", run, each(budget("per_instance", 90), at(F + ("instances",), 32 * 90), at(F + ("used",), 32 * 90 + 588348)), True) + case(33, "a prior of 87 bytes under 89 a lifecycle call", run, each(budget("per_instance", 89), at(F + ("instances",), 32 * 90), at(F + ("used",), 32 * 90 + 588348)), False) + case(33, "an init stopped after its 100-byte prior, under 103", init, each(own_prior(100), budget("per_instance", 103), at(F + ("instances",), 104), at(F + ("used",), 104)), True) + case(33, "an init stopped after its 100-byte prior, under 102", init, each(own_prior(100), budget("per_instance", 102), at(F + ("instances",), 104), at(F + ("used",), 104)), False) + # 34: what a fault's reason shows was charged + case(34, "a trap after nothing charged", init, each(at(R + ("fault", "fuel"), 0)), False) + case(34, "a result that is not one, after 2", fault, fault_of("bad_result", 2), True) + case(34, "a result that is not one, after 1", fault, fault_of("bad_result", 1), False) + case(34, "the stack past 2^23 after 788", fault, each(budget("per_call", 1000), fault_of("stack_exhausted", 788)), True) + case(34, "the stack past 2^23 after 787", fault, each(budget("per_call", 1000), fault_of("stack_exhausted", 787)), False) + # 35: the transcript of no returned call + case(35, "an ok run of no call, its transcript the prefix's", run, lambda b: (b["result"].update(callouts=0), b["result"]["fuel"].update(callouts={}, used=b["result"]["fuel"]["instances"])), True) + case(35, "an ok run of no call, its transcript the calls'", run, lambda b: (b["result"].update(callouts=0, transcript_blake3=b["result"]["transcript_blake3"][::-1]), b["result"]["fuel"].update(callouts={}, used=b["result"]["fuel"]["instances"])), False) + case(35, "a call's fault of no returned call, another transcript", fault, at(R + ("transcript_blake3",), "ab" * 32), False) + case(35, "a lifecycle fault, another transcript", init, at(R + ("transcript_blake3",), "ab" * 32), False) + # Places where a member's own rule and another's both read it: each verifier must state + # every rule it breaks, so that one reading it otherwise in one rule shows. + case(13, "per_job 2^53, alike in the result", run, budget("per_job", 2 ** 53), False) + case(6, "a module's blake3 in upper case, alike in the result", run, lambda b: (contract(b)["modules"][0].update(blake3=contract(b)["modules"][0]["blake3"].upper()), b["result"]["modules"][0].update(blake3=b["result"]["modules"][0]["blake3"].upper())), False) + + def named_one(b): + contract(b)["modules"][0]["name"] = 1 + b["result"]["modules"][0]["name"] = 1 + if "fault" in b["result"]: + b["result"]["fault"]["module"] = 1 + case(6, "a module named 1, the result's module 1.0", run, named_one, False, respell(R + ("modules", 0, "name"), "1.0", 1)) + case(6, "a module named 1, an init's fault in 1.0", init, named_one, False, respell(R + ("fault", "module"), "1.0", 1)) + case(6, "a module named 1, a prior's fault in 1.0", prior, named_one, False, respell(R + ("fault", "module"), "1.0", 1)) + case(26, "one instance for the run, a fault in shot 0.5", init, each(at(("request", "wasmq", "run", "state"), "job"), at(R + ("fault", "shot"), 0.5)), False) + case(18, "a fault in shot 0.5, after a call", init, each(at(R + ("fault", "shot"), 0.5), at(R + ("callouts",), 1)), False) + + # A lifecycle fault is one that has a phase: null among them, absent not. + case(26, "a fault whose phase is null, in shot 2 of one instance for the run", init, each(later_shot(2, state="job"), at(R + ("fault", "phase"), None)), False) + case(18, "a fault with no phase, in shot 2 of one instance for the run", init, each(later_shot(2, state="job"), lambda b: b["result"]["fault"].pop("phase")), False) + + # Every member of each genuine job given a value of another kind (half a unit off, below + # 0, past 2^53 - 1, null, a digest in upper case), sealed again. No member of a wasmQ job + # holds such a value: each verifier must refuse each, and both must state the same rules + # broken (the differential holds them to that), so that a rule's check read loosely in + # one, or a null read as absent, shows wherever another rule's check also refuses it. + def leaves(v, path): + if isinstance(v, dict): + for k, x in v.items(): + yield path + (k,), x + for y in leaves(x, path + (k,)): + yield y + elif isinstance(v, list): + for i, x in enumerate(v[:1]): + yield path + (i,), x + for y in leaves(x, path + (i,)): + yield y + for name, base in (("run", run), ("fault", fault), ("init", init), ("fault-run", frun), ("prior", prior), ("state-job", job)): + for part in ("request", "result"): + for path, v in leaves(base[part], (part,)): + # The receipt is the seal's; the format says what kind of job it is. + if "receipt" in path or path == ("result", "format"): + continue + where = "%s %s" % (name, "/".join(map(str, path))) + if isinstance(v, (int, float)) and not isinstance(v, bool): + case("kinds", "%s + 0.5" % where, base, at(path, v + 0.5), False) + case("kinds", "%s at -1" % where, base, at(path, -1), False) + case("kinds", "%s at 2^53" % where, base, at(path, 2 ** 53), False) + if isinstance(v, str) and re.fullmatch("[0-9a-f]{64}", v): + case("kinds", "%s in upper case" % where, base, at(path, v.upper()), False) + case("kinds", "%s null" % where, base, at(path, None), False) + return out + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--reference", help="the wai-quantum binary") @@ -224,7 +781,13 @@ def main(): ap.add_argument("--corpus", default=CORPUS) ap.add_argument("--extra", action="append", default=[], help="a directory of receipts both must find valid") ap.add_argument("--plant", help="run the second verifier with this planted defect; succeed only if it is caught") + ap.add_argument("--second", help="another second verifier's file in place of receipt_verify.py (a mutant of it)") + ap.add_argument("--wasmq-only", action="store_true", help="only what holds a wasmQ job: its receipts, their changes, the rules and the labelled cases") + ap.add_argument("--first", action="store_true", help="stop at the first stage that finds a disagreement") a = ap.parse_args() + global SECOND + if a.second: + SECOND = os.path.abspath(a.second) if not a.second_only and not a.reference: ap.error("--reference is required, unless --second-only") ref = os.path.abspath(a.reference) if a.reference else None @@ -239,20 +802,48 @@ def main(): problems.append("%s is in the manifest and not in the corpus" % f) # The corpus: both verifiers and the manifest, receipt by receipt. - files = [os.path.join(a.corpus, e["file"]) for e in manifest if e["file"] in present] - entries = [e for e in manifest if e["file"] in present] + entries = [e for e in manifest if e["file"] in present and (not a.wasmq_only or "wasmq" in e["file"])] + files = [os.path.join(a.corpus, e["file"]) for e in entries] r2 = run_second_files(files, a.plant) r1 = r2 if a.second_only else run_reference_files(ref, files) agree = 0 for e, x, y in zip(entries, r1, r2): + if not a.second_only: + same_rules(e["file"], x, y, problems) want = tuple(sorted(e["reasons"])) if said(x) != said(y) or said(x)[0] != want: problems.append("%s: the manifest says %s; the reference %s; the second verifier %s" % (e["file"], list(want), list(said(x)), list(said(y)))) else: agree += 1 + # A crafted wasmQ receipt that names one rule breaks that rule alone: one problem from the + # reference, and that rule alone from the second verifier, which numbers its rules. + alone = 0 + for e, x, y in zip(entries, r1, r2): + m = re.search(r"\(rule (\d+)\)$", e.get("what", "")) + if not m or not e["file"].startswith("jobs/crafted-wasmq-"): + continue + alone += 1 + for who, v in ((None if a.second_only else "the reference", x), ("the second verifier", y)): + if who and rules_of(v) != {int(m.group(1))}: + problems.append("%s breaks rule %s alone, but %s states %s" % (e["file"], m.group(1), who, (v.get("wasmq") or {}).get("problems"))) + print("corpus: %d crafted wasmQ receipts each break the one rule they name, and no other" % alone) valid = sum(1 for e in entries if not e["reasons"]) print("corpus: %d receipts (%d valid, %d not), %d agree %s" % (len(entries), valid, len(entries) - valid, agree, "with the manifest (second verifier only)" if a.second_only else "three ways (manifest, reference, second)")) if a.second_only: + g = generator() + problems += rules_stage(None, a.plant, (g.seal, g.public_key(g.DEMO).hex())) + problems += labelled_stage(None, a.plant) + return finish(a, problems) + if a.first and problems: + return finish(a, problems) + # The rules and the labelled cases first: each is short, and each has its verdict written. + g = generator() + seal = (g.seal, g.public_key(g.DEMO).hex()) + problems += rules_stage(ref, a.plant, seal) + if a.first and problems: + return finish(a, problems) + problems += labelled_stage(ref, a.plant) + if a.first and problems: return finish(a, problems) # Receipts a person holds: both must find them valid. @@ -268,8 +859,6 @@ def main(): # Every valid receipt, one member changed at a time. lines, meta = [], [] - g = generator() - seal = (g.seal, g.public_key(g.DEMO).hex()) for e in entries: if e["reasons"]: continue @@ -293,6 +882,7 @@ def main(): reasons = Counter() for (fname, what, expected), x, y in zip(meta, m1, m2): sx, sy = said(x), said(y) + same_rules("%s, %s" % (fname, what), x, y, problems) if sx != sy: problems.append("%s, %s: the reference %s; the second verifier %s" % (fname, what, list(sx), list(sy))) continue @@ -310,10 +900,67 @@ def main(): held += 1 print("changed: %d receipts from %d valid ones; %d agree (%d refused, as every change inside the seal must be; %d valid, as every change outside it must be; %d at the edges of a number, judged only by the other verifier)" % (len(lines), valid, agree, caught, held, compared)) print("reasons given to the changed receipts: " + ", ".join("%s %d" % kv for kv in sorted(reasons.items()))) - return finish(a, problems) +def rules_stage(ref, plant, seal): + """Each verifier given every case of rule_cases: each must give the reasons written for + it (the second alone with --second-only).""" + cases = rule_cases(CORPUS, seal[0]) + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "rules.jsonl") + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(b if isinstance(b, str) else compact(b) for _, b, _ in cases) + "\n") + second = run_lines([sys.executable, "-I", SECOND] + (["--plant", plant] if plant else []) + ["--lines"], path, len(cases)) + first = run_lines([ref, "verify", "--lines"], path, len(cases)) if ref else second + problems = [] + for (what, _, want), x, y in zip(cases, first, second): + if ref: + same_rules(what, x, y, problems) + rule = re.match(r"rule (\d+), ", what) + for who, v in (("the reference", x), ("the second verifier", y)): + if tuple(sorted(v.get("reasons", []))) != tuple(want): + problems.append("%s: written %s; %s %s" % (what, want, who, sorted(v.get("reasons", [])))) + elif want and rule and rules_of(v) is not None and int(rule.group(1)) not in rules_of(v): + problems.append("%s: %s finds it breaks rules %s, not the rule it is written for" % (what, who, sorted(rules_of(v)))) + rules = len({w.split(",")[0] for w, _, _ in cases if re.match(r"rule \d+,", w)}) + kinds = sum(1 for w, _, _ in cases if w.startswith("rule kinds,")) + print("rules: %d cases, %d at the bounds of %d rules of a wasmQ job's form and %d of members of another kind, sealed again; %d as written, the same rules broken by both" % (len(cases), len(cases) - kinds, rules, kinds, len(cases) - len({p.split(":")[0] for p in problems}))) + return problems + + +def labelled_stage(ref, plant): + """The second verifier's own hand-labelled wasmQ cases (`receipt_verify.py --wasmq-cases`), + each as its exact text, given to both verifiers: each must give the reasons its author + wrote, so that one labelled corpus binds both. The labels come from receipt_verify.py as + written, whichever second verifier is under test.""" + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "cases.jsonl") + p = subprocess.run([sys.executable, "-I", ORIGINAL_SECOND, "--wasmq-cases", out], capture_output=True, text=True) + if p.returncode != 0: + raise SystemExit("receipt_verify.py --wasmq-cases failed: %s" % p.stderr.strip()[:400]) + with open(out, encoding="utf-8") as f: + cases = [json.loads(line) for line in f if line.strip()] + files = [] + for i, c in enumerate(cases): + path = os.path.join(tmp, "case-%04d.json" % i) + with open(path, "wb") as f: + f.write(base64.b64decode(c["raw"])) + files.append(path) + second = run_second_files(files, plant) + first = run_reference_files(ref, files) if ref else second + problems = [] + for c, x, y in zip(cases, first, second): + if ref: + same_rules("labelled %s" % c["name"], x, y, problems) + want = sorted(c["reasons"]) + for who, v in (("the reference", x), ("the second verifier", y)): + if sorted(v.get("reasons", [])) != want: + problems.append("labelled %s: written %s; %s %s" % (c["name"], want, who, sorted(v.get("reasons", [])))) + print("labelled: %d of the second verifier's hand-labelled wasmQ cases, given to both; %d as written" % (len(cases), len(cases) - len({p.split(":")[0] for p in problems}))) + return problems + + def finish(a, problems): if a.plant: if problems: diff --git a/wai/quantum-conformance/tools/receipt_verify.py b/wai/quantum-conformance/tools/receipt_verify.py index e6810d532..cd2b72ada 100644 --- a/wai/quantum-conformance/tools/receipt_verify.py +++ b/wai/quantum-conformance/tools/receipt_verify.py @@ -2,20 +2,38 @@ """receipt_verify.py: a second, independent verifier of WAI Quantum job receipts. Written from wai/extensions/quantum-providers.md §8 alone (the working text), and -sharing no code with the engine. Outside specifications it relies on: RFC 8785 -(JCS), RFC 8032 (Ed25519), the BLAKE3 specification, RFC 8259 (JSON), RFC 6901 -(JSON Pointer). Standard library only, Python 3.9 or later, run as `python3 -I`. +sharing no code with the engine. Its wasmQ rules (`wasmq_form`) were written from +wai/extensions/quantum-wasmq.md §6.5, which §8.4 names, by a reader who had not seen +the reference. Outside specifications it relies on: RFC 8785 (JCS), RFC 8032 +(Ed25519), the BLAKE3 specification, RFC 8259 (JSON), RFC 6901 (JSON Pointer). +Standard library only, Python 3.9 or later, run as `python3 -I`. python3 -I receipt_verify.py FILE... one verdict line per file python3 -I receipt_verify.py --lines FILE one compact job a line, one verdict a line python3 -I receipt_verify.py --self-test [ES_NUMBER_FILE] + python3 -I receipt_verify.py --wasmq-cases OUT.jsonl [ES_NUMBER_FILE] python3 -I receipt_verify.py --plant NAME ... a deliberate fault (below), never for use +--wasmq-cases writes every wasmQ case of the self-test whose whole-job verdict the +self-test writes, one JSON object a line, {"name", "raw" (base64 of the job's exact +bytes), "reasons" (sorted, [] when valid)}: what the self-test's author wrote from the +text, not a verdict of this file, for another verifier to be run on. It judges nothing. +The ES_NUMBER_FILE is taken for the self-test's shape and not read. + A verdict line is {"file", "valid", "reasons": [sorted §8.4 reasons], "key", "form"} with "key" the receipt's key hex when the receipt is of its form (else null) and "form" "rfc8785", "first" or null; a provider's result adds - "provider": {"form": "stated"|"earlier", "class", "read_as", "counts_source", "execution"}. + "provider": {"form": "stated"|"earlier", "class", "read_as", "counts_source", "execution"} +and a wasmQ job (its result's format is wai.quantum.wasmq.result/1) that was judged +(not unreadable, not_a_job or no_receipt) adds, as quantum-wasmq §6.5 says a verifier +states beside the verdict, + "wasmq": {"outcome", "problems": [...], "notes": [...]} +with "outcome" the sealed result's status (null when it is not a string), "problems" +each §6.5 rule the job breaks, one entry a rule ("rule 14: ..."), the first 64 and then +"and N more", and "notes" where the body's unsealed status, method and other members +differ from what is sealed or are missing. Neither changes "valid" or "reasons". A set +is no wasmQ job, and states none for its items. The exit status is 0 only when every verdict is valid. The reasons are exactly §8.4's and §8.6's: @@ -23,6 +41,7 @@ The reasons are exactly §8.4's and §8.6's: digest_mismatch, bad_key, bad_signature; inexact_number; provider_class, request_provider, request_shots, counts (every provider result); provider_members, counts_source, execution, compiled_program, cost_confirmation (the stated form); + wasmq_form (a wasmQ job, quantum-wasmq §6.5, whatever its method); set_items, set_item, set_root (a set, §8.6). --plant NAME exists only to prove that a differential against another verifier @@ -38,10 +57,16 @@ wrong by design. NAME is one of: no-canonical-spelling refuse an inexact whole number even in its canonical spelling set-seal-only check a set's seal, and nothing of its items or its root names-layout-only check compiled.qubit_names against the layouts only, not qasm3 -Without --plant the verifier is exactly §8's. + wasmq-unchecked skip the wasmQ rules (quantum-wasmq §6.5): no wasmq_form, and + the wasmq statement's problems empty (its outcome and notes, + which are no rules, still given) +Without --plant the verifier is exactly §8's (and quantum-wasmq §6.5's). Its reader's questions of the text, gap by gap, are what §8 now says: where §8 -left a choice, it was settled in the text, and this file follows the text. +left a choice, it was settled in the text, and this file follows the text. Where +quantum-wasmq §6.5 leaves one, the reading taken is marked SPEC-GAP [Wn]: [W2], [W5], [W9] +to [W14], and [W18] to [W22]. [W1], [W3], [W4], [W6], [W7], [W8], [W15], [W16] and [W17] +are gone: the text now settles them. """ import hashlib @@ -54,7 +79,7 @@ import time PLANTS_KNOWN = ("lenient-key", "no-alg", "last-duplicate", "no-inexact", "measured-simulator", "whole-cap", "lax-small-order-r", "no-canonical-spelling", "set-seal-only", - "names-layout-only") + "names-layout-only", "wasmq-unchecked") PLANTS = set() MAX_DEPTH = 127 # §8.2: nesting deeper than 127 is unreadable @@ -1034,7 +1059,668 @@ def provider_statement(result): } -# ===================================================================== 7. a job +# ============================================ 7. a wasmQ job (quantum-wasmq §6.5) +# +# Written from wai/extensions/quantum-wasmq.md §6.5 (with §2.2, §6.2 and §6.4 for the +# lists it names), by a reader who had not seen the reference. §8.4's `wasmq_form` +# holds a job to those rules when its result's format is wai.quantum.wasmq.result/1. +# Brought to §6.5 as it now stands (rule 18's call that stopped being one of the calls +# made; the statement of outcome, problems and notes) by a reader who had not seen it; +# and again for rules 20 to 35 (the ties between members: fuel, instances, the envelope's +# size, the calls' count, the least charge and the transcript of a run where no call +# returned) and rules 6, 8, 12, 14 and 18 as they now read, from the text alone. +# +# §6.5: a member is *present* when its object names it, whatever its value, null among +# them ("has no fault", "has a callout", "may have fault" read so). Every test of a +# member here is `name in obj` or `_get`, never `obj.get(name) is None`, so a member +# that is null is there; the statement's notes read the body's members the same way. + +WASMQ_FORMAT = "wai.quantum.wasmq.result/1" +WASMQ_PROFILE = "wai.quantum.wasmq/1" +# §6.4: the result's energy, the one sentence it gives. +WASMQ_ENERGY = "the runner reads no energy meter; fuel is the profile's measure of work" +# §6.5 rule 9 (§6.4): the result's members; it may also have `fault`. +WASMQ_RESULT_MEMBERS = frozenset(( + "format", "profile", "status", "method", "qubits", "shots", "shots_run", "seed", "counts", + "callouts", "fuel", "modules", "transcript_blake3", "accuracy", "energy", "receipt")) +# §6.2: the reasons a fault names. +WASMQ_FAULT_REASONS = ( + "fuel_exhausted", "stack_exhausted", "unreachable", "memory_out_of_bounds", + "table_out_of_bounds", "indirect_call_null", "indirect_call_type", "integer_divide_by_zero", + "integer_overflow", "invalid_conversion_to_integer", "bad_result") +WASMQ_PHASES = ("seed", "prior", "init", "call") +WASMQ_RULES = 35 +WASMQ_PROBLEMS_SHOWN = 64 # §6.5: the first 64 problems, then how many more +_ABSENT = object() # a member not present: equal to nothing + + +def _count(v): + """§6.5: a number whose value is a whole number from 0 to 2^53 − 1, however it is + written (1, 1.0 and 1e0 are one count; -0 is 0).""" + return isinstance(v, Num) and v.value.is_integer() and 0 <= v.value <= MAX_SAFE + + +def _count_in(v, low, high): + return _count(v) and low <= v.value <= high + + +def _n(v): + """A count's value as an exact integer, so that a sum or a product is exact.""" + return int(v.value) + + +def _or_zero(v): + """§6.5: a bound read "(0 when that is not a count)".""" + return _n(v) if _count(v) else 0 + + +def _text_is(v, *options): + return isinstance(v, str) and v in options + + +def _exactly(v, names): + """§6.5: every one named and no other; a value that is not an object has none.""" + return isinstance(v, dict) and set(v) == set(names) + + +def _obj(v): + """§6.5: a map that is not an object is read as an empty one, after its own rule.""" + return v if isinstance(v, dict) else {} + + +def _arr(v): + """§6.5: a list that is not an array is read as an empty one, after its own rule.""" + return v if isinstance(v, list) else [] + + +def _get(v, name): + """§6.5: a member's value when it is present (null among them), else _ABSENT.""" + return v[name] if isinstance(v, dict) and name in v else _ABSENT + + +def is_wasmq(result): + """§6.5: a result whose format is wai.quantum.wasmq.result/1 is a wasmQ job's.""" + return _text_is(result.get("format"), WASMQ_FORMAT) + + +def wasmq_broken(request, result): + """§6.5: the numbers of the rules a wasmQ job breaks; none when it keeps them all. + C is the request's wasmq, run C's run, r the result and F r's fuel. What decides is + what is sealed (the request, and the result less its receipt), but for rule 9's + `receipt` member and rule 10's canon, which name the receipt itself.""" + broken = set() + + def rule(number, holds): + if not holds: + broken.add(number) + + r = result + body = dict((k, v) for k, v in r.items() if k != "receipt") + c_given = _get(request, "wasmq") + c = _obj(c_given) + program_given, run_given = _get(c, "program"), _get(c, "run") + program, run = _obj(program_given), _obj(run_given) + budgets_given = _get(run, "fuel") + budgets = _obj(budgets_given) + # §6.5: "The budget per_call, per_job or per_instance is that member of run's fuel." + per_call, per_job, per_instance = (_get(budgets, k) for k in ("per_call", "per_job", "per_instance")) + modules_given, callouts_given = _get(c, "modules"), _get(c, "callouts") + modules, callouts = _arr(modules_given), _arr(callouts_given) + externs = [_get(co, "extern") for co in callouts] + f_given = _get(r, "fuel") + f = _obj(f_given) + shots, shots_run, status = _get(r, "shots"), _get(r, "shots_run"), _get(r, "status") + + # 1. No number in the request, or in the result less its receipt, has a magnitude + # past 2^53 − 1 (its value, the double it reads as). + rule(1, all(abs(n.value) <= MAX_SAFE for part in (request, body) for n in _numbers(part))) + + # 2. The request: exactly wasmq and envelope_blake3, the latter a digest. + rule(2, _exactly(request, ("wasmq", "envelope_blake3")) + and _lower_hex(request["envelope_blake3"], 64)) + + # 3. C, its program, run and run's fuel: exactly the members §2.2 gives. + rule(3, _exactly(c_given, ("profile", "program", "modules", "callouts", "run")) + and _exactly(program_given, ("blake3", "bytes", "inputs")) + and _exactly(run_given, ("shots", "seed", "method", "state", "fuel")) + and _exactly(budgets_given, ("per_call", "per_job", "per_instance"))) + + # 4. C's profile. + rule(4, _text_is(_get(c, "profile"), WASMQ_PROFILE)) + + # 5. The program: a digest; 1 to 2^20 bytes; inputs an object of whole numbers. + # SPEC-GAP: [W2] "at most 2^53 − 1 in size": a magnitude; an input may be negative + # (an int[k] input), so it is no count. + inputs = _get(program, "inputs") + rule(5, _lower_hex(_get(program, "blake3"), 64) + and _count_in(_get(program, "bytes"), 1, 2 ** 20) + and isinstance(inputs, dict) + and all(isinstance(v, Num) and v.value.is_integer() and abs(v.value) <= MAX_SAFE + for v in inputs.values())) + + # 6. The modules: a non-empty array, each module and each prior of its form, and the + # modules' bytes that are counts adding up to at most 2^19. A module's bytes is a count + # of at least 1 (the sum bounds each from above); a prior's bytes is a count, which + # rule 31 bounds, not this rule. + def prior_holds(p): + return (_exactly(p, ("name", "blake3", "bytes")) and isinstance(p["name"], str) + and _lower_hex(p["blake3"], 64) and _count(p["bytes"])) + + def module_holds(m): + return (_exactly(m, ("name", "blake3", "bytes", "priors")) and isinstance(m["name"], str) + and _lower_hex(m["blake3"], 64) and _count(m["bytes"]) and _n(m["bytes"]) >= 1 + and isinstance(m["priors"], list) and all(prior_holds(p) for p in m["priors"])) + + sizes = [_get(m, "bytes") for m in modules] + rule(6, isinstance(modules_given, list) and len(modules) > 0 + and all(module_holds(m) for m in modules) + and sum(_n(b) for b in sizes if _count(b)) <= 2 ** 19) + + # 7. The callouts: a non-empty array, each of its form, its module a count less than + # the number of modules; no extern twice; every module's index named by a callout. + def callout_holds(co): + return (_exactly(co, ("extern", "module", "export")) and isinstance(co["extern"], str) + and isinstance(co["export"], str) and _count(co["module"]) + and _n(co["module"]) < len(modules)) + + named = set(_n(_get(co, "module")) for co in callouts if _count(_get(co, "module"))) + extern_texts = [e for e in externs if isinstance(e, str)] + rule(7, isinstance(callouts_given, list) and len(callouts) > 0 + and all(callout_holds(co) for co in callouts) + and len(set(extern_texts)) == len(extern_texts) + and all(i in named for i in range(len(modules)))) + + # 8. run: shots 1 to 2^20, a seed, the method, the state, and each budget a count, + # per_call and per_instance each at most 2^39 (rule 30 holds per_job to less). + rule(8, _count_in(_get(run, "shots"), 1, 2 ** 20) and _count(_get(run, "seed")) + and _text_is(_get(run, "method"), "stabilizer") and _text_is(_get(run, "state"), "shot", "job") + and _count(per_job) and _count_in(per_call, 0, 2 ** 39) and _count_in(per_instance, 0, 2 ** 39)) + + # 9. r: exactly §6.4's members, and may have fault. + rule(9, set(r) - {"fault"} == WASMQ_RESULT_MEMBERS) + + # 10. The receipt is an object whose canon is rfc8785. §6.5: "Only what is sealed (the + # request, and the result less its receipt) and the form of its seal decide it": a + # receipt that is not an object, or of the first form, breaks it. + rule(10, _text_is(_get(_get(r, "receipt"), "canon"), "rfc8785")) + + # 11. r's constants, and its method, shots and seed equal to run's (numbers by value). + rule(11, _text_is(_get(r, "format"), WASMQ_FORMAT) and _text_is(_get(r, "profile"), WASMQ_PROFILE) + and _same(_get(r, "accuracy"), {"class": "exact"}) + and _same(_get(r, "energy"), {"unmetered": WASMQ_ENERGY}) + and all(_same(_get(r, k), _get(run, k)) for k in ("method", "shots", "seed"))) + + # 12. r's qubits a count from 1 to 2^15 (a program declares a qubit at least); its + # transcript_blake3 a digest. + rule(12, _count_in(_get(r, "qubits"), 1, 2 ** 15) and _lower_hex(_get(r, "transcript_blake3"), 64)) + + # 13. F: exactly its four members; its budget run's fuel; used at most per_job. + used, instances = _get(f, "used"), _get(f, "instances") + rule(13, _exactly(f_given, ("used", "instances", "budget", "callouts")) + and _same(_get(f, "budget"), budgets_given) + and _count(used) and _n(used) <= _or_zero(per_job)) + + # 14. F's callouts: an object; each member named by an extern of C's callouts, with + # exactly calls (at least 1), fuel and max (at most per_call and at most its own fuel), + # and fuel a count: rule 20, not this rule, bounds it by its calls and their max. + f_callouts_given = _get(f, "callouts") + f_callouts = _obj(f_callouts_given) + + extern_set = set(extern_texts) # a list here would make this quadratic + + def entry_holds(name, e): + return (name in extern_set and _exactly(e, ("calls", "fuel", "max")) + and _count(e["calls"]) and _n(e["calls"]) >= 1 + and _count(e["max"]) and _n(e["max"]) <= _or_zero(per_call) + and _count(e["fuel"]) and _n(e["max"]) <= _n(e["fuel"])) + + rule(14, isinstance(f_callouts_given, dict) + and all(entry_holds(k, v) for k, v in f_callouts.items())) + + # 15. F's instances and its callouts' fuel add up to used; their calls to r's callouts. + # SPEC-GAP: [W5] "Each of them is a count": every number the rule names, F's + # instances and used, r's callouts, and each member's fuel and calls. + fuels = [_get(e, "fuel") for e in f_callouts.values()] + calls = [_get(e, "calls") for e in f_callouts.values()] + r_callouts = _get(r, "callouts") + rule(15, all(_count(x) for x in [instances, used, r_callouts] + fuels + calls) + and _n(instances) + sum(_n(x) for x in fuels) == _n(used) + and sum(_n(x) for x in calls) == _n(r_callouts)) + + # 16. r's modules: as many as C's, each exactly name and blake3, equal to C's at its index. + r_modules_given = _get(r, "modules") + + def r_module_holds(i, m): + return (_exactly(m, ("name", "blake3")) and _lower_hex(m["blake3"], 64) + and _same(m["name"], _get(modules[i], "name")) + and _same(m["blake3"], _get(modules[i], "blake3"))) + + rule(16, isinstance(r_modules_given, list) and len(r_modules_given) == len(modules) + and all(r_module_holds(i, m) for i, m in enumerate(r_modules_given))) + + # 17. F's instances at most M × S × per_instance: S, over C's modules, 2 and its priors; + # M 1 for state job "whatever the status", for any other state shots_run, and 1 more + # when status is fault. + s = sum(2 + len(_arr(_get(m, "priors"))) for m in modules) + if _text_is(_get(run, "state"), "job"): + m_factor = 1 + else: + m_factor = _or_zero(shots_run) + (1 if _text_is(status, "fault") else 0) + rule(17, _count(instances) and _n(instances) <= m_factor * s * _or_zero(per_instance)) + + # 18. status ok: no fault, and every shot run; status fault: the fault of §6.2's form. + if _text_is(status, "ok"): + rule(18, "fault" not in r and _count(shots_run) and _count(shots) and _n(shots_run) == _n(shots)) + elif _text_is(status, "fault"): + rule(18, _fault_holds(_get(r, "fault"), shots, shots_run, modules, externs, + f_callouts, per_instance)) + else: + rule(18, False) # any other status breaks the form + + # 19. r's counts: an object of outcomes of 0 and 1 (the empty name among them), all of + # one length, each at least 1, adding up to shots_run. + counts_given = _get(r, "counts") + counts = _obj(counts_given) + rule(19, isinstance(counts_given, dict) + and all(_OUTCOME.fullmatch(k) for k in counts) and len(set(len(k) for k in counts)) <= 1 + and all(_count(v) and _n(v) >= 1 for v in counts.values()) + and _count(shots_run) and sum(_n(v) for v in counts.values()) == _n(shots_run)) + + # Rules 20 to 35 tie members to each other. In them *the fault* is r's fault when r's + # status is fault and that is an object, and there is none otherwise; a rule about the + # fault holds when there is none. A *call's fault* is one whose phase is call, a + # *lifecycle fault* "one that has a phase other than call": a phase of null is present + # (§6.5), so a lifecycle fault's; a fault that names no phase is neither. + fault_given = _get(r, "fault") + fault = fault_given if _text_is(status, "fault") and isinstance(fault_given, dict) else None + phase, reason = _get(fault, "phase"), _get(fault, "reason") + fault_fuel, fault_shot = _get(fault, "fuel"), _get(fault, "shot") + call_fault = fault is not None and _text_is(phase, "call") + lifecycle_fault = fault is not None and phase is not _ABSENT and not _text_is(phase, "call") + state_job = _text_is(_get(run, "state"), "job") + + # 20. Each member of F's callouts: s 1 when the fault is a call's fault whose callout is + # that member's name, else 0; e the fault's fuel when s is 1 (0 when it is no count), + # else 0. T, its fuel less e (the fuel of its calls that returned), is not less than 0, + # and is from k to k × max, k its calls less s (0 when that is below 0); and when max + # is more than e, T is at least max + k − 1. calls, fuel and max are counts. + def returned_holds(name, member): + stopped = 1 if call_fault and _same(_get(fault, "callout"), name) else 0 + calls, fuel, top = _get(member, "calls"), _get(member, "fuel"), _get(member, "max") + if not (_count(calls) and _count(fuel) and _count(top)): + return False + stopped_fuel = _or_zero(fault_fuel) if stopped else 0 # e + returned = _n(fuel) - stopped_fuel # T + returning = max(_n(calls) - stopped, 0) # k + return (returned >= 0 and returning <= returned <= returning * _n(top) + and (_n(top) <= stopped_fuel or returned >= _n(top) + returning - 1)) + + rule(20, all(returned_holds(name, member) for name, member in f_callouts.items())) + + # 21. F's instances is a count of at least W × P + E, the least making the instances took + # outside a stopped call. + # P: over every prior of every one of C's modules, 3 and the prior's bytes (0 when no + # count), and 1 more when the fault is a lifecycle fault in phase seed or init. + # W: r's shots_run (0 when no count), 1 more when there is a call's fault, and with + # state job that or 1, whichever is less. + # E: when the fault is a lifecycle fault, the same sum of 3 and the bytes over the priors + # of each module before the one it names (the first of C's modules with that name), + # and, in phase init, over that module's own; 0 otherwise, and when no module has it. + # SPEC-GAP: [W19] "E ... the same sum, of 3 and its bytes": read as the sum over priors + # alone, without P's 1 more for a fault in seed or init, as "of 3 and its bytes" says. + def passing_of(priors): + return sum(3 + _or_zero(_get(p, "bytes")) for p in priors) + + all_priors = [p for m in modules for p in _arr(_get(m, "priors"))] + passing = passing_of(all_priors) + (1 if lifecycle_fault and _text_is(phase, "seed", "init") else 0) + made_whole = _or_zero(shots_run) + (1 if call_fault else 0) + if state_job: + made_whole = min(made_whole, 1) + stopped_priors = [] # the priors E counts: those the stopped set passed whole + if lifecycle_fault: + named = next((i for i, m in enumerate(modules) if _same(_get(fault, "module"), _get(m, "name"))), + None) + if named is not None: + upto = named + 1 if _text_is(phase, "init") else named + stopped_priors = [p for m in modules[:upto] for p in _arr(_get(m, "priors"))] + least_instances = made_whole * passing + passing_of(stopped_priors) + rule(21, _count(instances) and _n(instances) >= least_instances) + + # 22. A call's fault's module equals the name of C's module whose index is the module of + # each of C's callouts whose extern is the fault's callout (rule 7 allows one); it + # fails when there is no such callout, or no such module. + def callout_module_name(co): + index = _get(co, "module") + if _count(index) and _n(index) < len(modules): + return _get(modules[_n(index)], "name") + return _ABSENT # no such module: equal to nothing + + stopped_in = ([co for co in callouts if _same(_get(co, "extern"), _get(fault, "callout"))] + if call_fault else []) + rule(22, not call_fault + or (len(stopped_in) > 0 + and all(_same(_get(fault, "module"), callout_module_name(co)) for co in stopped_in))) + + # 23. A fault whose phase is prior has as its module the name of one of C's modules + # whose priors is not empty (a priors that is no array read as an empty one). + rule(23, fault is None or not _text_is(phase, "prior") + or any(len(_arr(_get(m, "priors"))) > 0 and _same(_get(fault, "module"), _get(m, "name")) + for m in modules)) + + # 24. A fault whose reason is bad_result has the phase call or prior. + rule(24, fault is None or not _text_is(reason, "bad_result") or _text_is(phase, "call", "prior")) + + # 25. A lifecycle fault's fuel is a count, and F's instances (a count) is at least it + # added to rule 21's W × P + E. + rule(25, not lifecycle_fault + or (_count(fault_fuel) and _count(instances) + and _n(instances) >= _n(fault_fuel) + least_instances)) + + # 26. When run's state is job, a lifecycle fault's shot is the count 0. + first_shot = _count_in(fault_shot, 0, 0) + rule(26, not (lifecycle_fault and state_job) or first_shot) + + # 27. A lifecycle fault whose shot is the count 0 comes with r's callouts the count 0. + rule(27, not (lifecycle_fault and first_shot) or _count_in(r_callouts, 0, 0)) + + # 28. A fuel_exhausted fault used its whole limit: with budget call its fuel is a count + # equal to the budget per_call, with instance to per_instance (each 0 when no count); + # with run, F's used is a count equal to the budget per_job (a count), and the fault's + # fuel is a count, less than its own limit: per_call in a call's fault, per_instance in + # a lifecycle fault, each read as 0 when it is not a count ("a fault with no phase has + # no limit here, and breaks rule 18"). + # SPEC-GAP: [W18] the rule says what the whole limit is for the budgets call, instance + # and run only; a fault of fuel_exhausted with no budget, a budget of null or of + # another value (rule 18 then fails) is held to nothing here. + budget = _get(fault, "budget") + if fault is None or not _text_is(reason, "fuel_exhausted"): + whole_limit = True + elif _text_is(budget, "call"): + whole_limit = _count(fault_fuel) and _n(fault_fuel) == _or_zero(per_call) + elif _text_is(budget, "instance"): + whole_limit = _count(fault_fuel) and _n(fault_fuel) == _or_zero(per_instance) + elif _text_is(budget, "run"): + # Its own limit: per_call in a call's fault, per_instance in a lifecycle fault, each + # 0 when no count (a budget of null among them); a fault with no phase has none. + # (`limited` is a flag of its own: a budget of null reads as Python's None.) + limited = call_fault or lifecycle_fault + own_limit = per_call if call_fault else per_instance + whole_limit = (_count(used) and _count(per_job) and _n(used) == _n(per_job) + and _count(fault_fuel) + and (not limited or _n(fault_fuel) < _or_zero(own_limit))) + else: + whole_limit = True + rule(28, whole_limit) + + # 29. When r's counts has a name, the longest, in characters (Unicode code points), is at + # most 2^20 long, and run's shots (0 when no count) times that length at most 2^28 + # (counts read as an empty object when it is no object, after rule 19 has failed). + longest = max((len(name) for name in counts), default=None) + rule(29, longest is None + or (longest <= 2 ** 20 and _or_zero(_get(run, "shots")) * longest <= 2 ** 28)) + + # 30. The part of a run's work a receipt shows, at most 2^42: + # S × 2·n·w + I × Σ (4096 × (1 + 2p) + b) + 131072 × Σ b + 4097 × K + 8 × the budget + # per_job, S run's shots, n r's qubits, w ⌈n/32⌉ or 1 when that is 0, I 1 for state job + # and S otherwise, each sum over C's modules (p its number of priors, b its bytes), K + # r's callouts; every number that is not a count read as 0. + shots_asked = _or_zero(_get(run, "shots")) + qubits = _or_zero(_get(r, "qubits")) + tableau_words = -(-qubits // 32) or 1 + instances_counted = 1 if state_job else shots_asked + code = [_or_zero(_get(m, "bytes")) for m in modules] + instance_work = sum(4096 * (1 + 2 * len(_arr(_get(m, "priors")))) + b for m, b in zip(modules, code)) + work = (shots_asked * 2 * qubits * tableau_words + instances_counted * instance_work + + 131072 * sum(code) + 4097 * _or_zero(r_callouts) + 8 * _or_zero(per_job)) + rule(30, work <= 2 ** 42) + + # 31. The envelope the contract names is at most 2^27 bytes: 20 bytes of magic and + # lengths; the manifest, §2.1's members in order with no space, 192 bytes and the + # decimal digits of r's qubits and of run's shots and seed (each 0 when no count); + # C in RFC 8785 form; and the frames (the program's bytes, each module's and each + # prior's, each 0 when no count), added. + # SPEC-GAP: [W20] "C in RFC 8785 form": a request with no wasmq member (rule 2 then + # fails) has no C, read as no bytes; any other value of wasmq, an object or not, is + # written in its canonical text (§8.2) and counted in its UTF-8 bytes. + manifest = 192 + sum(len(str(_or_zero(v))) for v in (_get(r, "qubits"), _get(run, "shots"), + _get(run, "seed"))) + contract = 0 if c_given is _ABSENT else len(text_of(c_given, True).encode("utf-8")) + frames = (_or_zero(_get(program, "bytes")) + sum(code) + + sum(_or_zero(_get(p, "bytes")) for p in all_priors)) + rule(31, 20 + manifest + contract + frames <= 2 ** 27) + + # 32. r's callouts is at most 2^21 × H (each 0 when no count), H r's shots_run and 1 more + # when there is a call's fault: the shots that ran steps. + ran_steps = _or_zero(shots_run) + (1 if call_fault else 0) + rule(32, _or_zero(r_callouts) <= 2 ** 21 * ran_steps) + + # 33. Each prior passed whole fits one lifecycle call's limit: 3 and its bytes (0 when no + # count) at most the budget per_instance (0 when no count), for every prior of every + # one of C's modules when W (rule 21) is at least 1, and for each prior E counts + # otherwise. + passed_whole = all_priors if made_whole >= 1 else stopped_priors + rule(33, all(3 + _or_zero(_get(p, "bytes")) <= _or_zero(per_instance) for p in passed_whole)) + + # 34. A fault's fuel, when it is a count, is at least what its reason shows was charged: + # 1 for every reason but fuel_exhausted, 2 for bad_result, 788 for stack_exhausted. + # SPEC-GAP: [W21] "1 for every reason but fuel_exhausted": read of the reason the fault + # has, whatever its value; one §6.2 does not name, or one not a string (null among + # them), shows 1 (rule 18 fails for it). A fault that has no reason member shows + # nothing, and is held to no least here (rule 18 fails for it). + if fault is None or reason is _ABSENT or _text_is(reason, "fuel_exhausted"): + least_charged = 0 + elif _text_is(reason, "bad_result"): + least_charged = 2 + elif _text_is(reason, "stack_exhausted"): + least_charged = 788 + else: + least_charged = 1 + rule(34, not _count(fault_fuel) or _n(fault_fuel) >= least_charged) + + # 35. When no call returned (r's callouts the count 0, or the count 1 with a call's + # fault), r's transcript_blake3 is BLAKE3 over the bytes wai:wasmq-transcript\x01 and, + # when there is a fault, the bytes fault, its shot (u64), its reason's UTF-8 bytes and + # its fuel (u64), little-endian (shot and fuel 0 when no count, the reason no bytes + # when it is no string). + # SPEC-GAP: [W22] "\x01" read as the one byte 0x01 (as §8.2 names its domain byte), not + # the four characters; "the bytes fault" as the five ASCII bytes of the word. + if _count(r_callouts) and (_n(r_callouts) == 0 or (_n(r_callouts) == 1 and call_fault)): + transcript = b"wai:wasmq-transcript\x01" + if fault is not None: + transcript += (b"fault" + struct.pack("= _n(fuel) + + +def check_wasmq(request, result, reasons): + """§8.4 wasmq_form: a wasmQ job's request or result breaks a rule of quantum-wasmq + §6.5. Returns the numbers of the rules it breaks, none for another job.""" + if "wasmq-unchecked" in PLANTS: # PLANT: skip the wasmQ rules + return [] + broken = wasmq_broken(request, result) if is_wasmq(result) else [] + if broken: + reasons.add("wasmq_form") + return broken + + +# §6.5: "problems, each rule the job breaks, in words": one entry a rule, in this file's +# words (another verifier's are its own; the words are no part of the verdict). +WASMQ_RULE_WORDS = { + 1: "a number in the request, or in the result less its receipt, has a magnitude past 2^53 - 1", + 2: "the request is not exactly wasmq and envelope_blake3, envelope_blake3 a digest", + 3: "the contract, its program, its run or run's fuel has not exactly the members §2.2 gives", + 4: "the contract's profile is not wai.quantum.wasmq/1", + 5: "the program's blake3, bytes or inputs break their form", + 6: "the contract's modules break their form, or their bytes add up past 2^19", + 7: "the contract's callouts break their form, name an extern twice, or leave a module unnamed", + 8: "run's shots, seed, method, state or a budget breaks its form", + 9: "the result has not exactly the members of §6.4 (and may have fault)", + 10: "the receipt is not an object whose canon is rfc8785", + 11: "the result's format, profile, accuracy or energy is not §6.4's, or its method, shots " + "or seed is not run's", + 12: "the result's qubits is not a count from 1 to 2^15, or its transcript_blake3 is no digest", + 13: "the result's fuel has not exactly used, instances, budget and callouts, its budget is " + "not run's fuel, or used is past the budget per_job", + 14: "a member of fuel's callouts is no extern of the contract, or its calls, fuel and max " + "break their form or bounds", + 15: "fuel's instances and its callouts' fuel do not add up to used, or their calls to the " + "result's callouts", + 16: "the result's modules are not the contract's, name and blake3 at each index", + 17: "fuel's instances are past M x S x the budget per_instance", + 19: "the counts are not an object of outcomes of 0 and 1, of one length, each at least 1, " + "adding up to shots_run", + 20: "a member of fuel's callouts has a fuel of its returned calls (its fuel less the " + "stopped call's) below 0, outside 1 to max for each call that returned, or below max " + "and 1 for each other when the most was a returned call's", + 21: "fuel's instances are no count of at least W x P + E, the least that making the " + "instances took outside a stopped call", + 22: "a call's fault names another module than its callout's, or no callout or module of " + "the contract", + 23: "a fault in phase prior names no module of the contract that has a prior", + 24: "a fault of bad_result is in another phase than call or prior", + 25: "a lifecycle fault's fuel is no count, or with rule 21's W x P + E is past fuel's " + "instances", + 26: "with state job, a lifecycle fault's shot is not the count 0", + 27: "a lifecycle fault at shot 0 comes with the result's callouts other than the count 0", + 28: "a fault of fuel_exhausted did not use its whole limit (call, instance, or the run's " + "per_job used up below the call's own)", + 29: "the counts' longest outcome is past 2^20 bits, or run's shots times its length past 2^28", + 30: "the work the receipt shows (tableaux, instances, compiling, calls, per_job) is past " + "2^42", + 31: "the envelope the contract names (magic and lengths, manifest, contract, frames) is " + "past 2^27 bytes", + 32: "the result's callouts are past 2^21 for each shot that ran steps", + 33: "a prior passed whole does not fit within per_instance (3 and its bytes)", + 34: "a fault's fuel is below what its reason shows was charged (1, 2 for bad_result, 788 " + "for stack_exhausted)", + 35: "no call returned, and the transcript_blake3 is not the one such a run gives", +} + + +def _rule_words(number, result): + if number != 18: + return "rule %d: %s" % (number, WASMQ_RULE_WORDS[number]) + status = _get(result, "status") + if _text_is(status, "ok"): + words = "the status is ok, but the result has a fault, or not every shot ran" + elif _text_is(status, "fault"): + words = ("the status is fault, but the fault breaks §6.2's form or disagrees with the run " + "(its reason, shot, phase, module, fuel, callout or budget)") + else: + words = "the status is neither ok nor fault" + return "rule 18: " + words + + +def _first_and_more(items, shown=WASMQ_PROBLEMS_SHOWN): + """§6.5: the first 64, and then how many more.""" + # SPEC-GAP: [W11] "the first 64 of them and then how many more": how many more is + # one more entry of the list, "and N more", not a member of its own. One entry a + # rule gives at most 35, so a job never reaches it; the self-test calls it alone. + items = list(items) + if len(items) <= shown: + return items + return items[:shown] + ["and %d more" % (len(items) - shown)] + + +def _words(v): + """A value of the body or the result, written for a note.""" + return json.dumps(_plain(v), ensure_ascii=False, separators=(",", ":")) + + +def wasmq_notes(job, result): + """quantum-wasmq §6.4, §6.5: where the body's unsealed members differ from what is + sealed, or are missing. A note is no part of the verdict.""" + # SPEC-GAP: [W12] §6.4: the body's status is done when the result's status is ok and + # fault otherwise, so the body's status is held to that word, "otherwise" taken as + # it stands (a result's status that is absent, or no string, carries fault); its + # method to the result's method, as JSON values are equal (a result with no method + # leaves any method in the body differing). A body member that is null is present + # (§6.5), so it differs; only one the body does not name is missing. + notes = [] + status, method = _get(result, "status"), _get(result, "method") + carried = "done" if _text_is(status, "ok") else "fault" + for name, wanted, sealed in (("status", carried, status), ("method", method, method)): + said = _get(job, name) + if said is _ABSENT: + notes.append("the host's unsealed %s is missing" % name) + elif not _same(said, wanted): + note = "the host's unsealed %s says %s; the sealed result %s" % ( + name, _words(said), "has none" if sealed is _ABSENT else "says " + _words(sealed)) + if name == "status" and not _same(sealed, wanted): + # the sealed word is not the body's (ok carries done): say which it carries + note += ", and the body carries %s for that" % _words(wanted) + notes.append(note) + # SPEC-GAP: [W13] §6.4: "or any other member of the body": nothing sealed answers it, + # so each member beyond the four is noted whatever its value, in the canonical + # text's order of names. + for name in sorted(set(job) - {"request", "result", "status", "method"}, key=_utf16): + notes.append("the body's member %s is the host's word; nothing seals it" % _words(name)) + return notes + + +def wasmq_statement(job, result, broken): + """quantum-wasmq §6.5, §8.4: what a verifier states beside a wasmQ job's verdict: + outcome, problems and notes. None of it changes the verdict.""" + # SPEC-GAP: [W10] "outcome, the sealed result's status": given as that string, and + # as null when the status is absent or no string, so that the outcome is a word. + status = _get(result, "status") + return { + "outcome": status if isinstance(status, str) else None, + "problems": _first_and_more(_rule_words(n, result) for n in broken), + "notes": wasmq_notes(job, result), + } + + +# ===================================================================== 8. a job def _verdict(reasons, key=None, form=None): return {"valid": not reasons, "reasons": sorted(reasons), "key": key, "form": form} @@ -1049,16 +1735,20 @@ def judge(raw): if not (isinstance(job, dict) and isinstance(job.get("request"), dict) and isinstance(job.get("result"), dict)): return _verdict(["not_a_job"]) - # §8.2: only request and result are sealed; nothing else in the body is read. + # §8.2: only request and result are sealed; nothing else in the body decides (a + # wasmQ job's statement reads the rest for its notes, quantum-wasmq §6.5). request, result = job["request"], job["result"] if result.get("format") == SET_FORMAT: return judge_set(request, result) - return judge_job(request, result) + return judge_job(request, result, job) -def judge_job(request, result): - """§8.4: a job's request and result, already read, to its verdict.""" +def judge_job(request, result, job=None): + """§8.4: a job's request and result, already read, to its verdict. `job` is the body + they were read from: a wasmQ job judged from its body states beside its verdict + what quantum-wasmq §6.5 says; a set's item (no body of its own) states nothing.""" provider = result.get("method") == "provider" + broken = None if "receipt" not in result: verdict = _verdict(["no_receipt"]) else: @@ -1070,13 +1760,23 @@ def judge_job(request, result): check_provider(request, result, reasons) else: check_class(result, False, False, reasons) + # §8.4: a wasmQ job's result is held to wasmq_form whatever its method, and to + # the provider rules too when its method is provider. + broken = check_wasmq(request, result, reasons) verdict = _verdict(reasons, key, form) if provider: verdict["provider"] = provider_statement(result) + # §8.4, quantum-wasmq §6.5: a judged wasmQ job (not no_receipt, nor reported alone + # before it) states its outcome, its problems and the notes on its unsealed body. + # SPEC-GAP: [W14] a set's item that is a wasmQ job states none: the set's verdict is + # one line, the set (format wai.quantum.set/1) is no wasmQ job, and an item's own + # reasons are folded into set_item. + if broken is not None and job is not None and is_wasmq(result): + verdict["wasmq"] = wasmq_statement(job, result, broken) return verdict -# ===================================================================== 8. a set (§8.6) +# ===================================================================== 9. a set (§8.6) SET_FORMAT = "wai.quantum.set/1" _MERKLE_LEAF = b"moq-jwp:merkle-leaf\x01" @@ -1298,7 +1998,8 @@ def _read(path): def _usage(): sys.stderr.write("usage: receipt_verify.py [--plant NAME]... (FILE... | --lines FILE)\n" - " receipt_verify.py --self-test [ES_NUMBER_FILE]\n") + " receipt_verify.py --self-test [ES_NUMBER_FILE]\n" + " receipt_verify.py --wasmq-cases OUT.jsonl [ES_NUMBER_FILE]\n") return 2 @@ -1307,6 +2008,8 @@ def main(argv): return _usage() if argv[0] == "--self-test": return self_test(argv[1:]) + if argv[0] == "--wasmq-cases": + return write_wasmq_cases(argv[1:]) verdicts, ok, i = [], True, 0 while i < len(argv): arg = argv[i] @@ -1351,7 +2054,7 @@ def main(argv): return 0 if ok and all(v["valid"] for v in verdicts) else 1 -# ================================================================ 9. self-test +# =============================================================== 10. self-test _BLAKE3_VECTORS = ( # BLAKE3's test vectors: byte i is i mod 251 (0, "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"), @@ -1566,6 +2269,928 @@ def _set_body(seed, request, item_requests, errors=(), before_root=None, after_r return _seal_set(seed, request, head, items, form=form) +# ---------------------------------------------- the wasmQ cases (quantum-wasmq §6.5) +# +# The self-test's wasmQ jobs, each made and sealed here, with the reasons §8.4 gives it and +# the rules of quantum-wasmq §6.5 it breaks, both written by hand from the text, never from +# this file's verdict. `--wasmq-cases` writes the same bytes and reasons, so that another +# verifier is held to what the text says, not to what this one does. + +_WASMQ_SEED = bytes(range(32)) # the self-test's key (its `seed`), every case's +_WASMQ_FORM = ["wasmq_form"] +_WASMQ_DONE = '"status":"done","method":"stabilizer",' +_WASMQ_FAULTED = '"status":"fault","method":"stabilizer",' +_WASMQ_SEALED_LINE = '; the sealed result says ' +_WASMQ_UNCHECKED_NAME = "plant wasmq-unchecked: strict verdict" + + +def _wasmq_parts(fault): + """The request and result of a wasmQ job that keeps every rule of §6.5: a run of 32 + shots, state shot; or, with `fault`, one of 8 shots, state job, stopped in shot 2's call + of decode64 by a fault of the call's fuel. The numbers the ties of rules 20 to 35 ask: + the run's instances, 3000, are at least W × P = 32 × (3 + 7) = 320 (rule 21); each + member of its callouts has fuel from calls to calls × max, and at least max and 1 for + each other call (rule 20); its prior's 3 + 7 fits per_instance (rule 33). The fault's + run has W = 1 (state job), P = 10, at most its instances 400; the stopped call used + per_call, 400 (rule 28), and decode64's two calls that returned used 600, from 2 to + 2 × 400 (rule 20, its max not more than the stopped call's 400); decode64 is the extern + of callout 0, whose module 0 is the decoder (rule 22). In both a call returned, so rule + 35 leaves the transcript free. Either's work (rule 30) is some 2^37, and its envelope + (rule 31) some 2^15 bytes.""" + budget = {"per_call": 400 if fault else 10000000, "per_job": 17179869184, "per_instance": 100000000} + asked = {"wasmq": { + "profile": WASMQ_PROFILE, + "program": {"blake3": "a1" * 32, "bytes": 1104, "inputs": {"rounds": 3, "offset": -2}}, + "modules": [{"name": "decoder", "blake3": "b2" * 32, "bytes": 39442, + "priors": [{"name": "checks", "blake3": "c3" * 32, "bytes": 7}]}, + {"name": "planner", "blake3": "d4" * 32, "bytes": 2048, "priors": []}], + "callouts": [{"extern": "decode64", "module": 0, "export": "decode64"}, + {"extern": "plan", "module": 1, "export": "plan"}], + "run": {"shots": 8 if fault else 32, "seed": 1234, "method": "stabilizer", + "state": "job" if fault else "shot", "fuel": dict(budget)}}, + "envelope_blake3": "e5" * 32} + made = { + "format": WASMQ_FORMAT, "profile": WASMQ_PROFILE, "status": "ok", "method": "stabilizer", + "qubits": 17, "shots": 32, "shots_run": 32, "seed": 1234, + "counts": {"0101": 30, "1100": 2}, "callouts": 34, + "fuel": {"used": 9500, "instances": 3000, "budget": dict(budget), + "callouts": {"decode64": {"calls": 32, "fuel": 6400, "max": 200}, + "plan": {"calls": 2, "fuel": 100, "max": 60}}}, + "modules": [{"name": "decoder", "blake3": "b2" * 32}, {"name": "planner", "blake3": "d4" * 32}], + "transcript_blake3": "f6" * 32, "accuracy": {"class": "exact"}, + "energy": {"unmetered": WASMQ_ENERGY}} + if fault: # state job: stopped in shot 2's call of decode64 + made.update(status="fault", shots=8, shots_run=2, counts={"0101": 1, "1100": 1}, callouts=3, + fault={"reason": "fuel_exhausted", "shot": 2, "phase": "call", "module": "decoder", + "fuel": 400, "callout": "decode64", "budget": "call"}) + made["fuel"].update(used=1400, instances=400, + callouts={"decode64": {"calls": 3, "fuel": 1000, "max": 400}}) + return asked, made + + +def _wasmq_clone(v): + """A change's value copied, so that a case never changes another's.""" + if isinstance(v, dict): + return dict((k, _wasmq_clone(x)) for k, x in v.items()) + if isinstance(v, list): + return [_wasmq_clone(x) for x in v] + return v + + +_WASMQ_TRANSCRIPT = object() # a result's transcript_blake3: the one rule 35 gives + + +def _wasmq_u64(v): + """A count's 8 bytes, little-endian; 0 for what is no count (rule 35).""" + x = v.value if isinstance(v, Num) else v + if isinstance(x, bool) or not isinstance(x, (int, float)) or x != int(x) or not 0 <= x <= 2 ** 53 - 1: + x = 0 + return int(x).to_bytes(8, "little") + + +def _wasmq_transcript(made): + """§6.4, rule 35: the transcript of a run in which no call returned, written here from + §6.4's words: BLAKE3 over "wai:wasmq-transcript" and the byte 1, then, after a fault, + "fault", its shot (u64), its reason's UTF-8 bytes and its fuel (u64), little-endian.""" + data = "wai:wasmq-transcript".encode("ascii") + bytes([1]) + fault = made.get("fault") + if made.get("status") == "fault" and isinstance(fault, dict): + reason = fault.get("reason") + data += ("fault".encode("ascii") + _wasmq_u64(fault.get("shot")) + + (reason.encode("utf-8") if isinstance(reason, str) else b"") + + _wasmq_u64(fault.get("fuel"))) + return blake3(data).hex() + + +def _wasmq_job(fault=False, request=None, result=None, envelope=_WASMQ_DONE, **seal_options): + """A wasmQ job sealed here, each change a path (members joined by "__") and its value, + applied in order. A result's transcript_blake3 of _WASMQ_TRANSCRIPT is replaced, once + every change is made, by the transcript §6.4 gives the run (rule 35).""" + asked, made = _wasmq_parts(fault) + for target, changes in ((asked, request), (made, result)): + for path, value in (changes or {}).items(): + node, parts = target, path.split("__") + for part in parts[:-1]: + node = node[int(part)] if isinstance(node, list) else node[part] + last = int(parts[-1]) if isinstance(node, list) else parts[-1] + if value is _DELETE: + del node[last] + elif isinstance(node, list) and last == len(node): + node.append(_wasmq_clone(value)) + else: + node[last] = _wasmq_clone(value) + if made.get("transcript_blake3") is _WASMQ_TRANSCRIPT: + made["transcript_blake3"] = _wasmq_transcript(made) + return _seal(_WASMQ_SEED, _dump(asked), _dump(made), envelope=envelope, **seal_options) + + +def _wasmq_changed_after(raw, change): + """A job changed after its seal.""" + job = json.loads(raw.decode()) + change(job) + return json.dumps(job).encode() + + +def _wasmq_statement(outcome, problems=(), notes=()): + return {"outcome": outcome, "problems": list(problems), "notes": list(notes)} + + +def _wasmq_unchecked_job(): + """The job of the plant wasmq-unchecked: one rule (11) broken.""" + return _wasmq_job(result={"profile": "wai.quantum.wasmq/2"}) + + +def _wasmq_tables(): + """The wasmQ cases: (cases, statements, verdicts). + + cases: (name, job, the reasons, the rules of §6.5 it breaks, None for no wasmQ job); + statements: (name, job, the reasons, the statement beside its verdict); + verdicts: (name, job, the reasons, whether the whole verdict is those reasons alone). + Every reason and rule here is the text's, worked out by hand for the job's numbers.""" + form = _WASMQ_FORM + shot_state = {"wasmq__run__state": "shot"} + job_state = {"wasmq__run__state": "job"} + # A fault outside a call that is no fault of fuel: a trap of 100 fuel, with no callout and + # no budget. On the fault's job (instances 400) it keeps rule 25: in state shot, + # 100 + W × P + E = 100 + 2 × (10 + 1) + 0 in seed, + 10 more in init (the decoder's own + # prior); in state job, 100 + 1 × 11. + seed_trap = {"fault__reason": "unreachable", "fault__phase": "seed", "fault__callout": _DELETE, + "fault__budget": _DELETE, "fault__fuel": 100} + init_trap = dict(seed_trap, fault__phase="init") + # A call's trap (no budget) of 150 fuel: below decode64's max of 400. + call_trap = {"fault__reason": "unreachable", "fault__budget": _DELETE, "fault__fuel": 150} + # State job, stopped at the first shot by a trap in the decoder's seed: no shot ran, + # no call was made, and no instance was made whole (W = 0, rule 21; E = 0, the decoder + # first). No call returned, so its transcript is the one rule 35 gives. + first_shot = {"shots_run": 0, "counts": {}, "callouts": 0, "fuel__used": 50, "fuel__instances": 50, + "fuel__callouts": {}, "transcript_blake3": _WASMQ_TRANSCRIPT, + "fault": {"reason": "unreachable", "shot": 0, "phase": "seed", "module": "decoder", "fuel": 50}} + a_call_at_first = {"callouts": 1, "fuel__used": 60, + "fuel__callouts": {"decode64": {"calls": 1, "fuel": 10, "max": 10}}} + # The run's budget the limit of decode64's third call: per_job 1150, of which the + # instances used 400 and two calls 400 and 200, leaving 150 < per_call (400). + run_budget = {"fuel__budget__per_job": 1150, "fuel__used": 1150, "fuel__callouts__decode64__fuel": 750, + "fault__fuel": 150, "fault__budget": "run"} + # A fault in init of the instance's budget, state shot: per_instance 100, all used. + init_exhausted = {"fuel__budget__per_instance": 100, "fault__phase": "init", "fault__budget": "instance", + "fault__callout": _DELETE, "fault__fuel": 100} + # A fault in the decoder's prior, state shot. + prior_fault = {"fault__reason": "bad_result", "fault__phase": "prior", "fault__callout": _DELETE, + "fault__budget": _DELETE, "fault__fuel": 100} + # A fault in plan's call (the module of index 1), plan's one call the one that stopped. + plan_fault = {"callouts": 3, "fault__callout": "plan", "fault__module": "planner", + "fuel__callouts": {"decode64": {"calls": 2, "fuel": 600, "max": 400}, + "plan": {"calls": 1, "fuel": 400, "max": 400}}} + plan_fuel = {"calls": 2, "fuel": 100, "max": 60} + # The ok run with no call made: its instances all its fuel; its transcript is f6... but + # for a case that asks the one rule 35 gives. + no_call = {"callouts": 0, "fuel__callouts": {}, "fuel__used": 3000} + # The fault's run whose one call made is the call that stopped (shot 2's of decode64). + one_call = {"callouts": 1, "fuel__used": 800, + "fuel__callouts": {"decode64": {"calls": 1, "fuel": 400, "max": 400}}} + many_shots = {"wasmq__run__state": "job", "wasmq__run__shots": 2 ** 20} + many_made = {"shots": 2 ** 20, "shots_run": 2 ** 20} + # Rule 30: with the ok run's numbers (34 callouts, 4097 each), all but 8 × per_job comes + # to 5 440 169 634 (state shot, 17 qubits), 5 440 172 770 (33 qubits, w = 2) and + # 5 438 375 540 (state job, I = 1); none is a multiple of 8, so the most per_job leaves + # the work just under 2^42, and one more puts it just past. + work_at = 549075792683 # ⌊(2^42 − 5 440 169 634) / 8⌋: the work 2^42 − 6 + work_at_33 = 549075792291 # ⌊(2^42 − 5 440 172 770) / 8⌋: the work 2^42 − 6 + work_job = 549076016945 # ⌊(2^42 − 5 438 375 540) / 8⌋: the work 2^42 − 4 + # Rule 31: the envelope is 20 bytes, the manifest (192 + "17", "32" and "1234": 200 + # bytes), the contract in RFC 8785 form (786 bytes, its prior's bytes of 9 digits, state + # job, per_instance 2^28) and the frames, 1104 + 39442 + 2048 and the prior's bytes. At + # a prior of 134 174 128 bytes it is 2^27 exactly. State job, so that W = 1 and the + # instances hold one passing of the prior, 3 + its bytes (rule 21); per_instance 2^28, + # so that the prior fits one lifecycle call (rule 33). + envelope_prior = 2 ** 27 - (20 + 200 + 786 + 1104 + 39442 + 2048) + big_prior_asked = dict(job_state, wasmq__run__fuel__per_instance=2 ** 28) + big_prior = {"fuel__budget__per_instance": 2 ** 28, "fuel__instances": 3 + envelope_prior + 1, + "fuel__used": 3 + envelope_prior + 1 + 6500} + + cases = ( + ("wasmq: a run", _wasmq_job(), [], []), + ("wasmq: a run stopped by a fault", _wasmq_job(fault=True, envelope=_WASMQ_FAULTED), [], []), + ("wasmq: shots written 32.0", _wasmq_job(request={"wasmq__run__shots": 32.0}, + result={"shots": 32.0, "shots_run": 32.0}), [], []), + ("wasmq: shots written 3.2e1, a count 30e0", _wasmq_job(request={"wasmq__run__shots": Num("3.2e1", 32.0)}, + result={"counts__0101": Num("30e0", 30.0)}), [], []), + ("wasmq: a module index written -0 and 1e0", _wasmq_job(request={ + "wasmq__callouts__0__module": Num("-0", -0.0), "wasmq__callouts__1__module": Num("1e0", 1.0)}), [], []), + ("wasmq: the unsealed status and method changed", _wasmq_job(envelope='"status":"failed","method":7,'), + [], []), + ("wasmq: the unsealed status and method removed", _wasmq_job(envelope=""), [], []), + ("wasmq: a fault's unsealed status says done", _wasmq_job(fault=True), [], []), + ("wasmq: a member added to the receipt", _wasmq_job(receipt_extra={"note": "added", "x": [1]}), [], []), + ("wasmq: an input negative, at its size", _wasmq_job(request={ + "wasmq__program__inputs__offset": -(2 ** 53 - 1)}), [], []), + ("wasmq: an empty prior", _wasmq_job(request={"wasmq__modules__0__priors__0__bytes": 0}), [], []), + # Rule 12: a program declares a qubit at least. + ("wasmq rule 12: qubits 0", _wasmq_job(result={"qubits": 0}), form, [12]), + ("wasmq: qubits 1", _wasmq_job(result={"qubits": 1}), [], []), + ("wasmq: qubits 2^15", _wasmq_job(result={"qubits": 2 ** 15}), [], []), + ("wasmq: instances and used moved together", _wasmq_job(result={ + "fuel__instances": 3001, "fuel__used": 9501}), [], []), + ("wasmq: an outcome of no bits", _wasmq_job(result={"counts": {"": 32}}), [], []), + ("wasmq: a fault in init, of its instance's budget", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_instance=100), result=init_exhausted), [], []), + ("wasmq: a fault of the run's budget in a call", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1150}, result=run_budget), [], []), + ("wasmq: a trap in seed", _wasmq_job(fault=True, request=shot_state, result=seed_trap), [], []), + ("wasmq: state shot, a fault's instance counted (M = 2 + 1)", _wasmq_job(fault=True, request={ + "wasmq__run__state": "shot", "wasmq__run__fuel__per_instance": 27}, + result={"fuel__budget__per_instance": 27}), [], []), + ("wasmq: state job, one instance (M = 1)", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_instance": 80}, result={"fuel__budget__per_instance": 80}), [], []), + ("wasmq: a call's fuel below its callout's max", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason="integer_divide_by_zero", fuel__callouts__decode64__fuel=750, + fuel__used=1150)), [], []), + ("wasmq: a call's callout's max written 4e2, its fuel's", _wasmq_job(fault=True, result={ + "fuel__callouts__decode64__max": Num("4e2", 400.0)}), [], []), + ("wasmq: another format is not a wasmQ job's", _wasmq_job(result={"format": "other", "profile": "x"}), + [], None), + # Rule 1, beside the rule of the number it breaks: every number a job of the form + # holds is a count or an input, each held to 2^53 − 1 by a rule of its own. + ("wasmq rule 1: an input of 2^53", _wasmq_job(request={"wasmq__program__inputs__rounds": 2 ** 53}), + form, [1, 5]), + ("wasmq rule 1: an input of -2^53", _wasmq_job(request={"wasmq__program__inputs__offset": -2 ** 53}), + form, [1, 5]), + ("wasmq rule 1: a seed of 2^53", _wasmq_job(request={"wasmq__run__seed": 2 ** 53}, + result={"seed": 2 ** 53}), form, [1, 8]), + # Rules 2 to 19. + ("wasmq rule 2: envelope_blake3 in upper case", _wasmq_job(request={"envelope_blake3": "E5" * 32}), + form, [2]), + ("wasmq rule 2: a member more", _wasmq_job(request={"note": "x"}), form, [2]), + ("wasmq rule 2: envelope_blake3 missing", _wasmq_job(request={"envelope_blake3": _DELETE}), form, [2]), + ("wasmq rule 3: the program a member more", _wasmq_job(request={"wasmq__program__note": "x"}), form, [3]), + ("wasmq rule 3: the contract a member more", _wasmq_job(request={"wasmq__note": "x"}), form, [3]), + ("wasmq rule 3: run a member more", _wasmq_job(request={"wasmq__run__note": "x"}), form, [3]), + ("wasmq rule 3: run's fuel a member more, the budget alike", _wasmq_job( + request={"wasmq__run__fuel__extra": 1}, result={"fuel__budget__extra": 1}), form, [3]), + ("wasmq rule 4: another profile", _wasmq_job(request={"wasmq__profile": "wai.quantum.wasmq/2"}), + form, [4]), + ("wasmq rule 5: no bytes", _wasmq_job(request={"wasmq__program__bytes": 0}), form, [5]), + ("wasmq rule 5: past 2^20 bytes", _wasmq_job(request={"wasmq__program__bytes": 2 ** 20 + 1}), form, [5]), + ("wasmq rule 5: an input not whole", _wasmq_job(request={"wasmq__program__inputs__rounds": 1.5}), + form, [5]), + ("wasmq rule 5: inputs an array", _wasmq_job(request={"wasmq__program__inputs": []}), form, [5]), + ("wasmq rule 5: the program's digest in upper case", _wasmq_job(request={ + "wasmq__program__blake3": "A1" * 32}), form, [5]), + ("wasmq rule 6: a module of no bytes", _wasmq_job(request={"wasmq__modules__0__bytes": 0}), form, [6]), + ("wasmq: a module of one byte", _wasmq_job(request={"wasmq__modules__1__bytes": 1}), [], []), + ("wasmq rule 6: the modules past 2^19 bytes together", _wasmq_job(request={ + "wasmq__modules__0__bytes": 2 ** 19}), form, [6]), + # Rule 6 bounds the modules' bytes only by their sum: one module may hold most of it. + ("wasmq: the modules at 2^19 bytes together, one of them 2^19 - 2048", _wasmq_job(request={ + "wasmq__modules__0__bytes": 2 ** 19 - 2048}), [], []), + ("wasmq rule 6: the modules one byte past 2^19 together", _wasmq_job(request={ + "wasmq__modules__0__bytes": 2 ** 19 - 2047}), form, [6]), + # Rule 6 no longer bounds a prior's bytes: rule 31 does (the envelope past 2^27), rule + # 21 counts them in the instances' fuel (W × P = 32 × (3 + 2^27 + 1), past the + # instances' 3000), and rule 33 holds a prior passed whole to per_instance (10^8). + ("wasmq rules 21, 31 and 33: a prior past 2^27 bytes, no longer rule 6's", _wasmq_job(request={ + "wasmq__modules__0__priors__0__bytes": 2 ** 27 + 1}), form, [21, 31, 33]), + # A prior's bytes null is no count: rule 6 fails, and rule 21 reads it as 0 + # (W × P = 32 × 2, within the instances' 100). + ("wasmq rule 6: a prior's bytes null, read as 0 by rule 21", _wasmq_job( + request={"wasmq__modules__0__priors__0__bytes": None}, + result={"fuel__instances": 100, "fuel__used": 6600}), form, [6]), + ("wasmq rule 6: a prior a member more", _wasmq_job(request={"wasmq__modules__0__priors__0__note": "x"}), + form, [6]), + ("wasmq rule 6: priors an object", _wasmq_job(request={"wasmq__modules__0__priors": {}}), form, [6]), + ("wasmq rule 6: a module's name a number, alike in the result", _wasmq_job( + request={"wasmq__modules__1__name": 7}, result={"modules__1__name": 7}), form, [6]), + ("wasmq rule 7: a module no callout names", _wasmq_job(request={"wasmq__callouts__1__module": 0}), + form, [7]), + ("wasmq rule 7: a module index past the modules", _wasmq_job(request={"wasmq__callouts__1__module": 2}), + form, [7]), + ("wasmq rule 7: an extern twice", _wasmq_job(request={"wasmq__callouts__2": { + "extern": "decode64", "module": 1, "export": "decode"}}), form, [7]), + ("wasmq rule 7: a module index not whole", _wasmq_job(request={"wasmq__callouts__1__module": 1.5}), + form, [7]), + ("wasmq rule 7: an export not a string", _wasmq_job(request={"wasmq__callouts__0__export": 7}), + form, [7]), + ("wasmq rule 8: another state", _wasmq_job(request={"wasmq__run__state": "batch"}), form, [8]), + ("wasmq rule 8: another method, alike in the result", _wasmq_job( + request={"wasmq__run__method": "statevector"}, result={"method": "statevector"}), form, [8]), + ("wasmq rule 8: a seed below 0, alike in the result", _wasmq_job( + request={"wasmq__run__seed": -1}, result={"seed": -1}), form, [8]), + ("wasmq rule 8: a budget past 2^39", _wasmq_job(request={"wasmq__run__fuel__per_call": 2 ** 39 + 1}, + result={"fuel__budget__per_call": 2 ** 39 + 1}), + form, [8]), + ("wasmq: per_call and per_instance at 2^39", _wasmq_job( + request={"wasmq__run__fuel__per_call": 2 ** 39, "wasmq__run__fuel__per_instance": 2 ** 39}, + result={"fuel__budget__per_call": 2 ** 39, "fuel__budget__per_instance": 2 ** 39}), [], []), + ("wasmq rule 8: per_instance past 2^39", _wasmq_job( + request={"wasmq__run__fuel__per_instance": 2 ** 39 + 1}, + result={"fuel__budget__per_instance": 2 ** 39 + 1}), form, [8]), + # Rule 8 holds per_job to be a count only (rule 30 bounds it); null is no count, and + # rule 13 then reads it as 0, below the used 9500. + ("wasmq rule 8: per_job null, no count", _wasmq_job( + request={"wasmq__run__fuel__per_job": None}, result={"fuel__budget__per_job": None}), + form, [8, 13]), + ("wasmq rule 9: a member more", _wasmq_job(result={"note": "x"}), form, [9]), + ("wasmq rule 10: sealed in the first form", _wasmq_job(form="first"), form, [10]), + ("wasmq rule 11: another profile", _wasmq_job(result={"profile": "wai.quantum.wasmq/2"}), form, [11]), + ("wasmq rule 11: accuracy a member more", _wasmq_job(result={"accuracy__note": "x"}), form, [11]), + ("wasmq rule 11: another energy", _wasmq_job(result={"energy__unmetered": "x"}), form, [11]), + ("wasmq rule 11: another seed", _wasmq_job(result={"seed": 1235}), form, [11]), + ("wasmq rule 11: another method", _wasmq_job(result={"method": "statevector"}), form, [11]), + ("wasmq rule 12: past 2^15 qubits", _wasmq_job(result={"qubits": 2 ** 15 + 1}), form, [12]), + ("wasmq rule 12: the transcript's digest in upper case", _wasmq_job(result={ + "transcript_blake3": "F6" * 32}), form, [12]), + ("wasmq rule 13: fuel a member more", _wasmq_job(result={"fuel__note": "x"}), form, [13]), + ("wasmq rule 13: a budget not run's", _wasmq_job(result={"fuel__budget__per_call": 400}), form, [13]), + ("wasmq rule 13: used past per_job", _wasmq_job(request={"wasmq__run__fuel__per_job": 9499}, + result={"fuel__budget__per_job": 9499}), form, [13]), + ("wasmq rule 14: a member no extern names", _wasmq_job(result={"fuel__callouts__plan": _DELETE, + "fuel__callouts__plan2": plan_fuel}), + form, [14]), + # A max past its member's fuel is past what rule 20 lets the calls that returned hold: + # max 120 is more than e = 0, so T = 100 must be at least 120 + 2 - 1. + ("wasmq rule 14: max past its fuel", _wasmq_job(result={"fuel__callouts__plan__max": 120}), + form, [14, 20]), + # Rule 14 holds a member's fuel to be a count only: rule 20 bounds it by calls × max. + ("wasmq rule 20: fuel past calls × max, no longer rule 14's", _wasmq_job(result={ + "fuel__callouts__decode64__max": 199}), form, [20]), + ("wasmq rule 14: max past per_call", _wasmq_job(request={"wasmq__run__fuel__per_call": 150}, + result={"fuel__budget__per_call": 150}), form, [14]), + ("wasmq rule 14: no calls", _wasmq_job(result={"fuel__callouts__plan": {"calls": 0, "fuel": 0, "max": 0}, + "fuel__used": 9400, "callouts": 32}), form, [14]), + ("wasmq rule 15: used not the sum", _wasmq_job(result={"fuel__used": 9501}), form, [15]), + ("wasmq rule 15: callouts not the calls", _wasmq_job(result={"callouts": 35}), form, [15]), + ("wasmq rule 16: a module named otherwise", _wasmq_job(result={"modules__1__name": "other"}), form, [16]), + ("wasmq rule 16: a module missing", _wasmq_job(result={"modules__1": _DELETE}), form, [16]), + ("wasmq rule 16: a module a member more", _wasmq_job(result={"modules__0__bytes": 39442}), form, [16]), + ("wasmq rule 16: another digest", _wasmq_job(result={"modules__0__blake3": "d4" * 32}), form, [16]), + ("wasmq: instances at most M × S × per_instance (32 × 5 × 19)", _wasmq_job( + request={"wasmq__run__fuel__per_instance": 19}, result={"fuel__budget__per_instance": 19}), [], []), + ("wasmq rule 17: instances past M × S × per_instance (32 × 5 × 18)", _wasmq_job( + request={"wasmq__run__fuel__per_instance": 18}, result={"fuel__budget__per_instance": 18}), + form, [17]), + ("wasmq rule 17: state shot, past (2 + 1) × S × per_instance", _wasmq_job(fault=True, request={ + "wasmq__run__state": "shot", "wasmq__run__fuel__per_instance": 26}, + result={"fuel__budget__per_instance": 26}), form, [17]), + ("wasmq rule 17: state job, past 1 × S × per_instance", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_instance": 79}, result={"fuel__budget__per_instance": 79}), form, [17]), + ("wasmq rule 18: ok, a shot not run", _wasmq_job(result={"shots_run": 31, "counts__0101": 29}), form, [18]), + ("wasmq rule 18: ok, with a fault", _wasmq_job(result={"fault": { + "reason": "unreachable", "shot": 32, "phase": "seed", "module": "decoder", "fuel": 1}}), form, [18]), + # With status ok there is no fault for rules 20 to 31: had this one been taken, rule + # 27 would fail (a lifecycle fault at shot 0 beside 34 callouts). + ("wasmq rule 18: ok, with a lifecycle fault at shot 0, no fault of rules 20 to 31", _wasmq_job(result={ + "fault": {"reason": "unreachable", "shot": 0, "phase": "seed", "module": "decoder", "fuel": 1}}), + form, [18]), + ("wasmq rule 18: ok, with a fault of null", _wasmq_job(result={"fault": None}), form, [18]), + ("wasmq rule 18: another status", _wasmq_job(result={"status": "done"}), form, [18]), + # Status done, beside a fault: no fault for rules 20 to 35. Taken, rule 28 (budget run, + # used 802 not per_job) would fail. Rule 20 holds either way: with s = 0, T = 402 is + # max 400 and 1 for each other call; with s = 1, T = 2 = k. + ("wasmq rule 18: status done beside a fault, no fault of rules 20 to 35", _wasmq_job(fault=True, result={ + "status": "done", "fault__budget": "run", "fuel__callouts__decode64__fuel": 402, "fuel__used": 802}), + form, [18]), + ("wasmq rule 18: fault, and no fault", _wasmq_job(fault=True, result={"fault": _DELETE}), form, [18]), + ("wasmq rule 18: fault, with a fault of null", _wasmq_job(fault=True, result={"fault": None}), form, [18]), + ("wasmq rule 18: a reason §6.2 does not name", _wasmq_job(fault=True, result={"fault__reason": "timeout"}), + form, [18]), + ("wasmq rule 18: every shot run", _wasmq_job(fault=True, result={ + "shots_run": 8, "counts__0101": 7, "fault__shot": 8}), form, [18]), + ("wasmq rule 18: the fault's shot not shots_run", _wasmq_job(fault=True, result={"fault__shot": 1}), + form, [18]), + # A phase of another value is a lifecycle fault's (rules 20 to 35): in state job at + # shot 2, rule 26 fails too. Its instances hold its fuel and W × P (400 + 1 × 10, rule + # 25; no 1 more, its phase is not seed or init). + ("wasmq rule 18: another phase", _wasmq_job(fault=True, result={ + "fault__phase": "run", "fuel__instances": 410, "fuel__used": 1410}), form, [18, 26]), + # A call's fault's module is its callout's (rule 22): one no module has is not it. + ("wasmq rule 18: a module not the contract's", _wasmq_job(fault=True, result={"fault__module": "other"}), + form, [18, 22]), + # A call's fuel past per_call (400) is past its callout's max (400, rule 14), which + # rule 18 holds it to; of the call's budget, it is not per_call itself (rule 28). + ("wasmq rule 18: fuel past per_call in a call", _wasmq_job(fault=True, result={"fault__fuel": 401}), + form, [18, 28]), + # Rule 18 holds a call's fuel to per_call only through its callout's max, and the max + # to per_call is rule 14's: a trap of 400 under per_call 399, beside a max of 400. + ("wasmq rule 14: a call's trap past per_call, held by its max", _wasmq_job( + fault=True, request={"wasmq__run__fuel__per_call": 399}, result={ + "fuel__budget__per_call": 399, "fault__reason": "unreachable", "fault__budget": _DELETE}), + form, [14]), + # Rule 18 no longer holds a fault's fuel within F's used: rule 25 holds a lifecycle + # fault's within the instances' (and rule 20 a call's within its member's). + ("wasmq rule 25: a lifecycle fault's fuel past F's used, no longer rule 18's", _wasmq_job( + fault=True, request=shot_state, result=dict(init_trap, fault__fuel=1401)), form, [25]), + # Outside a call, rule 18 holds a fault's fuel to per_instance. + ("wasmq: a trap in seed at per_instance", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_instance=100), result=dict( + seed_trap, fault__fuel=100, fuel__budget__per_instance=100)), [], []), + ("wasmq rule 18: a trap in seed past per_instance", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_instance=100), result=dict( + seed_trap, fault__fuel=101, fuel__budget__per_instance=100)), form, [18]), + # With no callout, or one no callout of C has, there is no callout for rule 22. + ("wasmq rule 18: a call without its callout", _wasmq_job(fault=True, result={"fault__callout": _DELETE}), + form, [18, 22]), + ("wasmq rule 18: a callout no extern names", _wasmq_job(fault=True, result={"fault__callout": "plan2"}), + form, [18, 22]), + ("wasmq rule 18: a callout outside a call", _wasmq_job(fault=True, request=shot_state, result=dict( + init_trap, fault__callout="decode64")), form, [18]), + ("wasmq rule 18: fuel_exhausted without its budget", _wasmq_job(fault=True, result={ + "fault__budget": _DELETE}), form, [18]), + # Rule 28 says what a whole limit is for the budgets call, instance and run alone. + ("wasmq rule 18: fuel_exhausted, its budget null", _wasmq_job(fault=True, result={"fault__budget": None}), + form, [18]), + ("wasmq rule 18: a budget on a trap", _wasmq_job(fault=True, result={"fault__reason": "unreachable"}), + form, [18]), + # Budget instance: the fault's fuel is not per_instance (rule 28). + ("wasmq rule 18: an instance's budget in a call", _wasmq_job(fault=True, result={ + "fault__budget": "instance"}), form, [18, 28]), + ("wasmq rule 18: the fault a member more", _wasmq_job(fault=True, result={"fault__note": "x"}), form, [18]), + # The call that stopped is one of the calls made: its extern names a member of F's + # callouts whose max is at least the fault's fuel. + ("wasmq rule 18: a call's callout an extern with no member in F's callouts", _wasmq_job( + fault=True, result={"fault__callout": "plan", "fault__module": "planner"}), form, [18]), + ("wasmq rule 18: a call's fuel past its callout's max", _wasmq_job(fault=True, result={ + "fuel__callouts__decode64__max": 399}), form, [18]), + # A member that is null is present (§6.5). + ("wasmq rule 18: a call's callout null, present and no extern", _wasmq_job(fault=True, result={ + "fault__callout": None}), form, [18, 22]), + ("wasmq rule 18: a callout of null outside a call, present", _wasmq_job( + fault=True, request=shot_state, result=dict(init_trap, fault__callout=None)), form, [18]), + ("wasmq rule 18: a budget of null on a trap, present", _wasmq_job(fault=True, result={ + "fault__reason": "unreachable", "fault__budget": None}), form, [18]), + ("wasmq rule 19: outcomes of two lengths", _wasmq_job(result={"counts": {"0101": 30, "110": 2}}), + form, [19]), + ("wasmq rule 19: an outcome not of 0 and 1", _wasmq_job(result={"counts": {"0101": 30, "1102": 2}}), + form, [19]), + ("wasmq rule 19: a count of 0", _wasmq_job(result={"counts": {"0101": 32, "1100": 0}}), form, [19]), + ("wasmq rule 19: counts not shots_run", _wasmq_job(result={"counts__0101": 29}), form, [19]), + ("wasmq rule 19: counts an array", _wasmq_job(result={"counts": []}), form, [19]), + + # Rule 20: the fuel of the calls that returned, each at least 1 and at most max. + ("wasmq: plan's calls at 1 fuel each", _wasmq_job(result={ + "fuel__callouts__plan": {"calls": 2, "fuel": 2, "max": 1}, "fuel__used": 9402}), [], []), + ("wasmq rule 20: plan's two calls of 1 fuel together", _wasmq_job(result={ + "fuel__callouts__plan": {"calls": 2, "fuel": 1, "max": 1}, "fuel__used": 9401}), form, [20]), + ("wasmq: beside the stopped call, the calls that returned at 1 fuel each", _wasmq_job(fault=True, result={ + "fuel__callouts__decode64__fuel": 402, "fuel__used": 802}), [], []), + ("wasmq rule 20: beside the stopped call, two calls that returned of 1 fuel together", _wasmq_job( + fault=True, result={"fuel__callouts__decode64__fuel": 401, "fuel__used": 801}), form, [20]), + ("wasmq: beside a trap, the calls that returned at max each", _wasmq_job(fault=True, result=dict( + call_trap, fuel__callouts__decode64__fuel=950, fuel__used=1350)), [], []), + ("wasmq rule 20: beside a trap, the calls that returned past max each", _wasmq_job(fault=True, result=dict( + call_trap, fuel__callouts__decode64__fuel=951, fuel__used=1351)), form, [20]), + # The fault's fuel that is no count is read as 0: then 1000 is the returned calls' + # fuel, past 2 × 400; with 800, within it. + ("wasmq rule 20: the fault's fuel null, read as 0", _wasmq_job(fault=True, result=dict( + call_trap, fault__fuel=None)), form, [18, 20]), + ("wasmq rule 18: the fault's fuel null, rule 20 reading it as 0", _wasmq_job(fault=True, result=dict( + call_trap, fault__fuel=None, fuel__callouts__decode64__fuel=800, fuel__used=1200)), form, [18]), + + # Rule 21: the instances' fuel at least W × P + E (P = 3 + 7 = 10 but for a fault in + # seed or init; E = 0 but for a lifecycle fault). + ("wasmq: instances at W × P, state shot (W = 32)", _wasmq_job(result={ + "fuel__instances": 320, "fuel__used": 6820}), [], []), + ("wasmq rule 21: instances below W × P, state shot (W = 32)", _wasmq_job(result={ + "fuel__instances": 319, "fuel__used": 6819}), form, [21]), + ("wasmq: instances at W × P, a call's fault in state shot (W = 2 + 1)", _wasmq_job( + fault=True, request=shot_state, result={"fuel__instances": 30, "fuel__used": 1030}), [], []), + ("wasmq rule 21: instances below W × P, a call's fault in state shot (W = 2 + 1)", _wasmq_job( + fault=True, request=shot_state, result={"fuel__instances": 29, "fuel__used": 1029}), form, [21]), + ("wasmq: instances at W × P, state job (W = 1)", _wasmq_job(fault=True, result={ + "fuel__instances": 10, "fuel__used": 1010}), [], []), + ("wasmq rule 21: instances below W × P, state job (W = 1)", _wasmq_job(fault=True, result={ + "fuel__instances": 9, "fuel__used": 1009}), form, [21]), + # A lifecycle fault, state shot (W = 2): rule 25 holds its fuel beside W × P + E, so + # its least is rule 25's, and rule 21 fails only below W × P + E itself. + # A trap of 1 in the decoder's seed: P = 10 + 1 (its module exports wasmq-seed), E = 0. + ("wasmq: a trap in seed, the instances at its fuel and W × P (1 + 2 × 11)", _wasmq_job( + fault=True, request=shot_state, result=dict( + seed_trap, fault__fuel=1, fuel__instances=23, fuel__used=1023)), [], []), + ("wasmq rule 25: a trap in seed, the instances one below its fuel and W × P", _wasmq_job( + fault=True, request=shot_state, result=dict( + seed_trap, fault__fuel=1, fuel__instances=22, fuel__used=1022)), form, [25]), + # In the planner's init: E holds the decoder's prior, passed before it (3 + 7). + ("wasmq: a trap in the planner's init, the instances at its fuel, W × P and E (1 + 22 + 10)", + _wasmq_job(fault=True, request=shot_state, result=dict( + init_trap, fault__module="planner", fault__fuel=1, fuel__instances=33, fuel__used=1033)), [], []), + ("wasmq rule 25: a trap in the planner's init, the instances one below", _wasmq_job( + fault=True, request=shot_state, result=dict( + init_trap, fault__module="planner", fault__fuel=1, fuel__instances=32, fuel__used=1032)), + form, [25]), + ("wasmq rules 21 and 25: a trap in the planner's init, the instances below W × P + E", _wasmq_job( + fault=True, request=shot_state, result=dict( + init_trap, fault__module="planner", fault__fuel=1, fuel__instances=31, fuel__used=1031)), + form, [21, 25]), + # In the decoder's init: E holds its own prior, passed before its init. + ("wasmq: a trap in the decoder's init, its own prior in E (1 + 22 + 10)", _wasmq_job( + fault=True, request=shot_state, result=dict( + init_trap, fault__fuel=1, fuel__instances=33, fuel__used=1033)), [], []), + ("wasmq rule 25: a trap in the decoder's init, its own prior in E, one below", _wasmq_job( + fault=True, request=shot_state, result=dict( + init_trap, fault__fuel=1, fuel__instances=32, fuel__used=1032)), form, [25]), + # In the decoder's prior: no 1 more (P = 10), and E = 0: a bad_result of 2. + ("wasmq: a fault in the decoder's prior, the instances at its fuel and W × P (2 + 2 × 10)", _wasmq_job( + fault=True, request=shot_state, result=dict( + prior_fault, fault__fuel=2, fuel__instances=22, fuel__used=1022)), [], []), + ("wasmq rule 25: a fault in the decoder's prior, the instances one below", _wasmq_job( + fault=True, request=shot_state, result=dict( + prior_fault, fault__fuel=2, fuel__instances=21, fuel__used=1021)), form, [25]), + + # Rule 22: a call runs in its callout's module. + ("wasmq rule 22: a call's fault in another module than its callout's", _wasmq_job(fault=True, result={ + "fault__module": "planner"}), form, [22]), + ("wasmq: a call's fault in plan, of the module of index 1", _wasmq_job(fault=True, result=plan_fault), + [], []), + ("wasmq rule 22: a call's fault in plan, named in the module of index 0", _wasmq_job(fault=True, result=dict( + plan_fault, fault__module="decoder")), form, [22]), + ("wasmq rule 22: its callout's module past the modules", _wasmq_job(fault=True, request={ + "wasmq__callouts__0__module": 2}), form, [7, 22]), + ("wasmq rule 22: its extern on two callouts, of two modules", _wasmq_job(fault=True, request={ + "wasmq__callouts__2": {"extern": "decode64", "module": 1, "export": "decode"}}), form, [7, 22]), + + # Rule 23: a prior is passed only to a module that has one. + ("wasmq: a fault in the decoder's prior", _wasmq_job(fault=True, request=shot_state, result=prior_fault), + [], []), + ("wasmq rule 23: a fault in a prior of the planner, which has none", _wasmq_job( + fault=True, request=shot_state, result=dict(prior_fault, fault__module="planner")), form, [23]), + ("wasmq rule 23: a fault in a prior, priors an object read as none", _wasmq_job( + fault=True, request=dict(shot_state, wasmq__modules__0__priors={}), result=prior_fault), + form, [6, 23]), + + # Rule 24: only a call or a prior passes bytes or takes a result. + ("wasmq: bad_result in a call", _wasmq_job(fault=True, result={ + "fault__reason": "bad_result", "fault__budget": _DELETE}), [], []), + ("wasmq rule 24: bad_result in init", _wasmq_job(fault=True, request=shot_state, result=dict( + init_trap, fault__reason="bad_result")), form, [24]), + ("wasmq rule 24: bad_result in seed", _wasmq_job(fault=True, request=shot_state, result=dict( + seed_trap, fault__reason="bad_result")), form, [24]), + + # Rule 25: a lifecycle fault's fuel, beside W × P + E (2 × 11 for a trap in seed, state + # shot), within the instances' 400. + ("wasmq: a trap in seed of 378, at the instances' 400 with W × P", _wasmq_job( + fault=True, request=shot_state, result=dict(seed_trap, fault__fuel=378)), [], []), + ("wasmq rule 25: a trap in seed of 379, past the instances' 400 with W × P", _wasmq_job( + fault=True, request=shot_state, result=dict(seed_trap, fault__fuel=379)), form, [25]), + + # Rules 26 and 27: the first shot's instances, made before its first step. + ("wasmq: a trap in seed at the first shot, state job (W = 0)", _wasmq_job(fault=True, result=first_shot), + [], []), + ("wasmq: a trap in seed at a first shot written -0", _wasmq_job(fault=True, result=dict( + first_shot, fault__shot=Num("-0", -0.0))), [], []), + ("wasmq: a trap in seed at the first shot, state shot", _wasmq_job( + fault=True, request=shot_state, result=first_shot), [], []), + ("wasmq rule 26: a trap in seed at shot 1, state job", _wasmq_job(fault=True, result=dict( + seed_trap, shots_run=1, counts={"0101": 1}, fault__shot=1)), form, [26]), + ("wasmq: a trap in seed at shot 1, state shot", _wasmq_job(fault=True, request=shot_state, result=dict( + seed_trap, shots_run=1, counts={"0101": 1}, fault__shot=1)), [], []), + ("wasmq rule 26: a trap in seed at shot 2, state job", _wasmq_job(fault=True, result=seed_trap), + form, [26]), + ("wasmq rule 26: a trap in seed, its shot null", _wasmq_job(fault=True, result=dict( + first_shot, fault__shot=None)), form, [18, 26]), + # A phase of null is present, a value other than call: a lifecycle fault's (rule 26 + # fails at shot 2 of state job; its instances hold 400 + 1 × 10, rule 25). One that is + # absent is no lifecycle fault's. + ("wasmq rule 26: a fault's phase null, a lifecycle fault's", _wasmq_job(fault=True, result={ + "fault__phase": None, "fuel__instances": 410, "fuel__used": 1410}), form, [18, 26]), + ("wasmq rule 18: a fault's phase absent, no lifecycle fault's", _wasmq_job(fault=True, result={ + "fault__phase": _DELETE}), form, [18]), + # Rule 27 is no longer broken alone: at a lifecycle fault of shot 0, rule 18 makes + # shots_run 0, so no shot ran steps (H = 0) and rule 32 holds the callouts to 0 too. + ("wasmq rule 27: a trap in seed at the first shot, beside a call", _wasmq_job(fault=True, result=dict( + first_shot, **a_call_at_first)), form, [27, 32]), + ("wasmq rule 27: a trap in seed at the first shot, beside a call, state shot", _wasmq_job( + fault=True, request=shot_state, result=dict(first_shot, **a_call_at_first)), form, [27, 32]), + + # Rule 28: a fault of fuel used its whole limit. + ("wasmq rule 28: the call's budget, one short of per_call", _wasmq_job(fault=True, result={ + "fault__fuel": 399}), form, [28]), + ("wasmq rule 28: the instance's budget, one short of per_instance", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_instance=101), result=dict( + init_exhausted, fuel__budget__per_instance=101)), form, [28]), + ("wasmq rule 28: the run's budget, per_job not used up", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1151}, result=dict(run_budget, fuel__budget__per_job=1151)), + form, [28]), + ("wasmq: the run's budget in a call, one below per_call", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1399}, result=dict( + run_budget, fuel__budget__per_job=1399, fuel__used=1399, fuel__callouts__decode64__fuel=999, + fault__fuel=399)), [], []), + ("wasmq rule 28: the run's budget in a call, at per_call", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1400}, result=dict( + run_budget, fuel__budget__per_job=1400, fuel__used=1400, fuel__callouts__decode64__fuel=1000, + fault__fuel=400)), form, [28]), + ("wasmq: the run's budget in init", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_job=1400), result={ + "fuel__budget__per_job": 1400, "fault__phase": "init", "fault__callout": _DELETE, + "fault__budget": "run", "fault__fuel": 50}), [], []), + ("wasmq rule 28: the run's budget in init, at per_instance", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_job=1400, wasmq__run__fuel__per_instance=50), result={ + "fuel__budget__per_job": 1400, "fuel__budget__per_instance": 50, "fault__phase": "init", + "fault__callout": _DELETE, "fault__budget": "run", "fault__fuel": 50}), form, [28]), + # The run's budget: "F's used is a count equal to the budget per_job (a count)": a + # per_job of null is no count, so rule 28 fails though used is 0 (rule 13 reads the + # null as 0, and holds). + ("wasmq rule 28: the run's budget, per_job null, nothing used", _wasmq_job( + fault=True, request={"wasmq__run__fuel__per_job": None}, result=dict( + first_shot, fuel__budget__per_job=None, fuel__used=0, fuel__instances=0, + fault__reason="fuel_exhausted", fault__fuel=0, fault__budget="run")), form, [8, 28]), + # The run's budget, its own limit no count: per_call (or per_instance) read as 0, so + # the fault's fuel is not less than it (rule 28), beside rule 8 and the rules that + # read that budget (14: max at most per_call; 17 and 18: at most per_instance). + ("wasmq rule 28: the run's budget in a call, per_call null", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1150, "wasmq__run__fuel__per_call": None}, result=dict( + run_budget, fuel__budget__per_call=None)), form, [8, 14, 28]), + ("wasmq rule 28: the run's budget in a call, per_call null in the contract alone", _wasmq_job( + fault=True, request={"wasmq__run__fuel__per_job": 1150, "wasmq__run__fuel__per_call": None}, + result=run_budget), form, [8, 13, 14, 28]), + ("wasmq rule 28: the run's budget in a call, per_call 400.5", _wasmq_job(fault=True, request={ + "wasmq__run__fuel__per_job": 1150, "wasmq__run__fuel__per_call": 400.5}, result=dict( + run_budget, fuel__budget__per_call=400.5)), form, [8, 14, 28]), + ("wasmq rule 28: the run's budget in init, per_instance null", _wasmq_job(fault=True, request=dict( + shot_state, wasmq__run__fuel__per_job=1400, wasmq__run__fuel__per_instance=None), result={ + "fuel__budget__per_job": 1400, "fuel__budget__per_instance": None, "fault__phase": "init", + "fault__callout": _DELETE, "fault__budget": "run", "fault__fuel": 50}), + form, [8, 17, 18, 28, 33]), + # "A fault with no phase has no limit here, and breaks rule 18": its fuel 150 at + # per_instance 150 is not held by rule 28. + ("wasmq rule 18: the run's budget, its phase absent, no limit of rule 28's", _wasmq_job( + fault=True, request={"wasmq__run__fuel__per_job": 1150, "wasmq__run__fuel__per_instance": 150}, + result=dict(run_budget, fuel__budget__per_instance=150, fault__phase=_DELETE)), form, [18]), + + # Rule 29: a program's bits, and shots times them. + ("wasmq: an outcome of 2^20 bits", _wasmq_job(result={"counts": {"0" * 2 ** 20: 32}}), [], []), + ("wasmq rule 29: an outcome of 2^20 + 1 bits", _wasmq_job(result={"counts": {"0" * (2 ** 20 + 1): 32}}), + form, [29]), + ("wasmq: 2^20 shots of 2^8 bits, 2^28", _wasmq_job(request=many_shots, result=dict( + many_made, counts={"0" * 2 ** 8: 2 ** 20})), [], []), + ("wasmq rule 29: 2^20 shots of 2^8 + 1 bits, past 2^28", _wasmq_job(request=many_shots, result=dict( + many_made, counts={"0" * (2 ** 8 + 1): 2 ** 20})), form, [29]), + + # Rule 30: the work a receipt shows. + # At 2^42 + 2 only with the calls' 4097 × 34: without them it would be 2^42 - 139 296. + ("wasmq: the work at 2^42 - 6", _wasmq_job(request={"wasmq__run__fuel__per_job": work_at}, + result={"fuel__budget__per_job": work_at}), [], []), + ("wasmq rule 30: the work at 2^42 + 2", _wasmq_job(request={"wasmq__run__fuel__per_job": work_at + 1}, + result={"fuel__budget__per_job": work_at + 1}), + form, [30]), + ("wasmq rule 30: per_job at 2^39", _wasmq_job( + request={"wasmq__run__fuel__per_job": 2 ** 39}, result={"fuel__budget__per_job": 2 ** 39}), + form, [30]), + # Rule 8 no longer bounds per_job: past 2^39 it breaks rule 30 alone. + ("wasmq rule 30: per_job past 2^39, no longer rule 8's", _wasmq_job( + request={"wasmq__run__fuel__per_job": 2 ** 39 + 1}, result={"fuel__budget__per_job": 2 ** 39 + 1}), + form, [30]), + ("wasmq: the work at 2^42 - 6, 33 qubits (w = 2)", _wasmq_job( + request={"wasmq__run__fuel__per_job": work_at_33}, + result={"qubits": 33, "fuel__budget__per_job": work_at_33}), [], []), + ("wasmq rule 30: the work at 2^42 + 2, 33 qubits (w = 2)", _wasmq_job( + request={"wasmq__run__fuel__per_job": work_at_33 + 1}, + result={"qubits": 33, "fuel__budget__per_job": work_at_33 + 1}), form, [30]), + ("wasmq: the work at 2^42 - 4, state job (I = 1)", _wasmq_job( + request=dict(job_state, wasmq__run__fuel__per_job=work_job), + result={"fuel__budget__per_job": work_job}), [], []), + ("wasmq rule 30: the work at 2^42 + 4, state job (I = 1)", _wasmq_job( + request=dict(job_state, wasmq__run__fuel__per_job=work_job + 1), + result={"fuel__budget__per_job": work_job + 1}), form, [30]), + + # Rule 31: the envelope the contract names, at most 2^27 bytes. One more byte past it: + # a prior's, a digit of the manifest's qubits (17 to 170), or one of the contract's + # own (a module's name, the same in the result). + ("wasmq: the envelope at 2^27 bytes", _wasmq_job(request=dict( + big_prior_asked, wasmq__modules__0__priors__0__bytes=envelope_prior), result=big_prior), [], []), + ("wasmq rule 31: the envelope at 2^27 + 1 bytes, a prior's", _wasmq_job(request=dict( + big_prior_asked, wasmq__modules__0__priors__0__bytes=envelope_prior + 1), result=big_prior), + form, [31]), + ("wasmq rule 31: the envelope at 2^27 + 1 bytes, a digit of qubits", _wasmq_job(request=dict( + big_prior_asked, wasmq__modules__0__priors__0__bytes=envelope_prior), result=dict( + big_prior, qubits=170)), form, [31]), + ("wasmq rule 31: the envelope at 2^27 + 1 bytes, a byte of the contract", _wasmq_job(request=dict( + big_prior_asked, wasmq__modules__0__priors__0__bytes=envelope_prior, + wasmq__modules__1__name="planners"), result=dict(big_prior, modules__1__name="planners")), + form, [31]), + + # Rule 20: when max is more than e, the most was a returned call's, and each other + # returned call used 1 at least: T at least max + k - 1. + ("wasmq: plan's fuel at its max and 1 for its other call (60 + 1)", _wasmq_job(result={ + "fuel__callouts__plan__fuel": 61, "fuel__used": 9461}), [], []), + ("wasmq rule 20: plan's fuel below its max and 1 for its other call", _wasmq_job(result={ + "fuel__callouts__plan__fuel": 60, "fuel__used": 9460}), form, [20]), + ("wasmq: beside a trap of 150, the calls that returned at max and 1 (400 + 1)", _wasmq_job( + fault=True, result=dict(call_trap, fuel__callouts__decode64__fuel=551, fuel__used=951)), [], []), + # e is the stopped call's fuel for its own member alone: plan's two calls beside + # decode64's call fault are held as their own (s = 0, e = 0: T = 100, at least 60 + 1). + ("wasmq: a call's fault in decode64 beside plan's calls, held as their own", _wasmq_job( + fault=True, result={"callouts": 5, "fuel__used": 1500, + "fuel__callouts__plan": {"calls": 2, "fuel": 100, "max": 60}}), [], []), + ("wasmq rule 20: beside a trap of 150, the calls that returned below max and 1", _wasmq_job( + fault=True, result=dict(call_trap, fuel__callouts__decode64__fuel=550, fuel__used=950)), + form, [20]), + + # Rule 32: the callouts at most 2^21 for each shot that ran steps: H = 2 shots run and + # the one a call's fault stopped. decode64's calls that returned used 1 each beside the + # stopped call's 400 (its max, so rule 20's last bound does not apply). + ("wasmq: callouts at 2^21 × 3", _wasmq_job(fault=True, result={ + "callouts": 3 * 2 ** 21, "fuel__used": 800 + 3 * 2 ** 21 - 1, "fuel__callouts__decode64": { + "calls": 3 * 2 ** 21, "fuel": 400 + 3 * 2 ** 21 - 1, "max": 400}}), [], []), + ("wasmq rule 32: callouts at 2^21 × 3 + 1", _wasmq_job(fault=True, result={ + "callouts": 3 * 2 ** 21 + 1, "fuel__used": 800 + 3 * 2 ** 21, "fuel__callouts__decode64": { + "calls": 3 * 2 ** 21 + 1, "fuel": 400 + 3 * 2 ** 21, "max": 400}}), form, [32]), + + # Rule 33: a prior passed whole fits one lifecycle call's limit, 3 and its 7 bytes. The + # ok run's instances 1000 keep rule 17 (32 × 5 × 9 = 1440). + ("wasmq: a prior at per_instance (3 + 7 = 10)", _wasmq_job( + request={"wasmq__run__fuel__per_instance": 10}, + result={"fuel__budget__per_instance": 10, "fuel__instances": 1000, "fuel__used": 7500}), [], []), + ("wasmq rule 33: a prior past per_instance (3 + 7, per_instance 9)", _wasmq_job( + request={"wasmq__run__fuel__per_instance": 9}, + result={"fuel__budget__per_instance": 9, "fuel__instances": 1000, "fuel__used": 7500}), form, [33]), + # With no set of instances made whole (W = 0), only the priors E counts: a trap in the + # planner's init at the first shot passed the decoder's prior whole (E = 10). + ("wasmq: a trap in the planner's init at the first shot, the decoder's prior at per_instance", _wasmq_job( + fault=True, request={"wasmq__run__fuel__per_instance": 10}, result=dict( + first_shot, fault__phase="init", fault__module="planner", fault__fuel=1, fuel__instances=11, + fuel__used=11, fuel__budget__per_instance=10)), [], []), + ("wasmq rule 33: a trap in the planner's init at the first shot, the decoder's prior past per_instance", + _wasmq_job(fault=True, request={"wasmq__run__fuel__per_instance": 9}, result=dict( + first_shot, fault__phase="init", fault__module="planner", fault__fuel=1, fuel__instances=11, + fuel__used=11, fuel__budget__per_instance=9)), form, [33]), + # A trap in the decoder's seed at the first shot passed no prior: E counts none. + ("wasmq: a trap in the decoder's seed at the first shot, per_instance below a prior none passed", + _wasmq_job(fault=True, request={"wasmq__run__fuel__per_instance": 9}, result=dict( + first_shot, fault__fuel=5, fuel__instances=5, fuel__used=5, fuel__budget__per_instance=9)), [], []), + + # Rule 34: a fault's fuel at least what its reason shows was charged. A call's trap, + # its calls that returned at 600 (rule 20: from 401 to 800 beside a max of 400). + ("wasmq: a call's trap of 1", _wasmq_job(fault=True, result=dict( + call_trap, fault__fuel=1, fuel__callouts__decode64__fuel=601, fuel__used=1001)), [], []), + ("wasmq rule 34: a call's trap of 0", _wasmq_job(fault=True, result=dict( + call_trap, fault__fuel=0, fuel__callouts__decode64__fuel=600, fuel__used=1000)), form, [34]), + ("wasmq: a call's bad_result of 2", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason="bad_result", fault__fuel=2, fuel__callouts__decode64__fuel=602, + fuel__used=1002)), [], []), + ("wasmq rule 34: a call's bad_result of 1", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason="bad_result", fault__fuel=1, fuel__callouts__decode64__fuel=601, + fuel__used=1001)), form, [34]), + # stack_exhausted in seed, state shot: its fuel and W × P (2 × 11) within the instances. + ("wasmq: stack_exhausted of 788 in seed", _wasmq_job(fault=True, request=shot_state, result=dict( + seed_trap, fault__reason="stack_exhausted", fault__fuel=788, fuel__instances=810, + fuel__used=1810)), [], []), + ("wasmq rule 34: stack_exhausted of 787 in seed", _wasmq_job(fault=True, request=shot_state, result=dict( + seed_trap, fault__reason="stack_exhausted", fault__fuel=787, fuel__instances=810, + fuel__used=1810)), form, [34]), + # A reason §6.2 does not name, or null (present), shows 1; a fault with no reason shows + # nothing (rule 18 fails for each). + ("wasmq rule 34: a call's trap of 0, its reason timeout", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason="timeout", fault__fuel=0, fuel__callouts__decode64__fuel=600, + fuel__used=1000)), form, [18, 34]), + ("wasmq rule 34: a call's trap of 0, its reason null", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason=None, fault__fuel=0, fuel__callouts__decode64__fuel=600, + fuel__used=1000)), form, [18, 34]), + ("wasmq rule 18: a call's trap of 0, no reason, held to no least", _wasmq_job(fault=True, result=dict( + call_trap, fault__reason=_DELETE, fault__fuel=0, fuel__callouts__decode64__fuel=600, + fuel__used=1000)), form, [18]), + + # Rule 35: when no call returned, the transcript is the one §6.4 gives the run. + ("wasmq: a run of no call, its transcript the one of no call", _wasmq_job(result=dict( + no_call, transcript_blake3=_WASMQ_TRANSCRIPT)), [], []), + ("wasmq rule 35: a run of no call, another transcript", _wasmq_job(result=no_call), form, [35]), + ("wasmq: a run of no call written 0e0, its transcript the one of no call", _wasmq_job(result=dict( + no_call, callouts=Num("0e0", 0.0), transcript_blake3=_WASMQ_TRANSCRIPT)), [], []), + ("wasmq rules 12 and 35: a run of no call, its transcript in upper case", _wasmq_job(result=dict( + no_call, transcript_blake3=_wasmq_transcript({"status": "ok"}).upper())), form, [12, 35]), + ("wasmq: a call's fault the one call made, its transcript the fault's", _wasmq_job(fault=True, result=dict( + one_call, transcript_blake3=_WASMQ_TRANSCRIPT)), [], []), + ("wasmq rule 35: a call's fault the one call made, another transcript", _wasmq_job( + fault=True, result=one_call), form, [35]), + # One call made, and a lifecycle fault: that call returned, so the transcript is free. + ("wasmq: a trap in seed at shot 1 beside one call that returned, its transcript free", _wasmq_job( + fault=True, request=shot_state, result=dict( + seed_trap, shots_run=1, counts={"0101": 1}, fault__shot=1, callouts=1, fuel__used=600, + fuel__callouts={"decode64": {"calls": 1, "fuel": 200, "max": 200}})), [], []), + # The fault's members in the transcript: a reason null or absent no bytes, a fuel null 0. + ("wasmq rule 18: a trap at the first shot, its reason null, no bytes in the transcript", _wasmq_job( + fault=True, result=dict(first_shot, fault__reason=None)), form, [18]), + ("wasmq rule 18: a trap at the first shot, no reason, no bytes in the transcript", _wasmq_job( + fault=True, result=dict(first_shot, fault__reason=_DELETE)), form, [18]), + ("wasmq rules 18 and 25: a trap at the first shot, its fuel null, 0 in the transcript", _wasmq_job( + fault=True, result=dict(first_shot, fault__fuel=None)), form, [18, 25]), + + # wasmq_form beside §8.4's other reasons, never instead of them. + ("wasmq: inexact, beside", _wasmq_job(request={"wasmq__run__seed": Num("9007199254740993", 2.0 ** 53)}, + result={"seed": Num("9007199254740993", 2.0 ** 53)}), + ["inexact_number", "wasmq_form"], [1, 8]), + ("wasmq: a class other methods may not have, beside", _wasmq_job(result={"accuracy__class": "simulated"}), + ["provider_class", "wasmq_form"], [11]), + ("wasmq: method provider, held to the provider rules too", _wasmq_job(result={"method": "provider"}), + ["counts", "provider_class", "request_provider", "request_shots", "wasmq_form"], [11]), + ("wasmq: receipt_form, the canon kept", _wasmq_job(receipt_extra={"alg": "ed448"}), ["receipt_form"], []), + ("wasmq: receipt_form, the canon null, beside", _wasmq_job(receipt_extra={"canon": None}), + ["receipt_form", "wasmq_form"], [10]), + ) + + # quantum-wasmq §6.5: beside a judged wasmQ job's verdict, its outcome, its problems + # (one a rule) and the notes on its unsealed body; none of it changes the verdict. + statement, said = _wasmq_statement, _WASMQ_SEALED_LINE + statements = ( + ("a run", _wasmq_job(), [], statement("ok")), + ("a run stopped by a fault", _wasmq_job(fault=True, envelope=_WASMQ_FAULTED), [], statement("fault")), + ("a job breaking one rule", _wasmq_job(result={"fuel__callouts__plan": _DELETE, + "fuel__callouts__plan2": plan_fuel}), form, + statement("ok", ["rule 14: " + WASMQ_RULE_WORDS[14]])), + ("a call fault past its callout's max", _wasmq_job( + fault=True, envelope=_WASMQ_FAULTED, result={"fuel__callouts__decode64__max": 399}), form, + statement("fault", [_rule_words(18, {"status": "fault"})])), + ("a job breaking two of the ties", _wasmq_job(fault=True, envelope=_WASMQ_FAULTED, result={ + "fault__module": "planner", "fault__fuel": 399}), form, + statement("fault", ["rule 22: " + WASMQ_RULE_WORDS[22], "rule 28: " + WASMQ_RULE_WORDS[28]])), + ("a fault's unsealed status says done: valid, one note", _wasmq_job(fault=True), [], + statement("fault", notes=['the host\'s unsealed status says "done"' + said + '"fault"'])), + ("the unsealed status and method changed", _wasmq_job(envelope='"status":"failed","method":7,'), [], + statement("ok", notes=[ + 'the host\'s unsealed status says "failed"' + said + '"ok", and the body carries "done" for that', + 'the host\'s unsealed method says 7' + said + '"stabilizer"'])), + ("the unsealed status and method missing", _wasmq_job(envelope=""), [], + statement("ok", notes=["the host's unsealed status is missing", "the host's unsealed method is missing"])), + ("the unsealed status null: present, so differing", _wasmq_job(envelope='"status":null,"method":"stabilizer",'), + [], statement("ok", notes=[ + 'the host\'s unsealed status says null' + said + '"ok", and the body carries "done" for that'])), + ("members beyond the four, one null", _wasmq_job( + envelope='"why":null,"status":"done","method":"stabilizer","id":"j-7",'), [], + statement("ok", notes=['the body\'s member "id" is the host\'s word; nothing seals it', + 'the body\'s member "why" is the host\'s word; nothing seals it'])), + ("a sealed status no string: outcome null", _wasmq_job(result={"status": 7}), form, + statement(None, ["rule 18: the status is neither ok nor fault"], + ['the host\'s unsealed status says "done"' + said + '7, and the body carries "fault" for that'])), + ("a sealed status missing: outcome null", _wasmq_job( + result={"status": _DELETE}, envelope=_WASMQ_FAULTED), form, + statement(None, ["rule 9: " + WASMQ_RULE_WORDS[9], "rule 18: the status is neither ok nor fault"])), + ("receipt_form: judged, so stated", _wasmq_job(receipt_extra={"alg": "ed448"}), ["receipt_form"], + statement("ok")), + ("method provider: held to the provider rules, and stated", _wasmq_job(result={"method": "provider"}), + ["counts", "provider_class", "request_provider", "request_shots", "wasmq_form"], + statement("ok", ["rule 11: " + WASMQ_RULE_WORDS[11]], + ['the host\'s unsealed method says "stabilizer"' + said + '"provider"'])), + ) + + run = _wasmq_job() + asked, made = _wasmq_parts(False) + verdicts = ( + ("wasmq: changed after its seal, the form kept: digest_mismatch alone", _wasmq_changed_after( + run, lambda job: job["result"].update(qubits=18)), ["digest_mismatch"], False), + ("wasmq: changed after its seal, the form broken: beside", _wasmq_changed_after( + run, lambda job: job["result"].update(profile="x")), ["digest_mismatch", "wasmq_form"], False), + ("wasmq: no receipt, alone", _wasmq_changed_after( + run, lambda job: (job["result"].pop("receipt"), job["result"].update(profile="x"))), + ["no_receipt"], False), + ("wasmq: not a job, alone", _wasmq_changed_after(run, lambda job: job.update(request=[])), + ["not_a_job"], False), + ("wasmq: a set's item, valid", _set_body( + _WASMQ_SEED, {"programs": [asked]}, [asked], item_result=_dump(made)), [], False), + ("wasmq: a set's item breaking a rule", _set_body( + _WASMQ_SEED, {"programs": [asked]}, [asked], item_result=_dump(dict(made, profile="x"))), + ["set_item"], False), + # Reported alone, a job's form is not read, and it states nothing: the whole verdict + # is the reason alone. + ("wasmq statement: unreadable states none", run.replace(b'{"status"', b'{"status":"x","status"'), + ["unreadable"], True), + ("wasmq statement: no_receipt states none", _wasmq_changed_after( + run, lambda job: job["result"].pop("receipt")), ["no_receipt"], True), + ("wasmq statement: not_a_job states none", _wasmq_changed_after( + run, lambda job: job.update(request=[])), ["not_a_job"], True), + ) + return cases, statements, verdicts + + +def wasmq_case_lines(): + """Every wasmQ case of the self-test whose whole-job verdict it writes: (name, the + exact bytes of the job's text, the reasons the self-test writes for it). It judges + nothing: the reasons are the self-test author's, from the text.""" + cases, statements, verdicts = _wasmq_tables() + out = [(name, raw, sorted(want)) for name, raw, want, _ in cases] + out += [("wasmq statement: " + name, raw, sorted(want)) for name, raw, want, _ in statements] + out += [(name, raw, sorted(want)) for name, raw, want, _ in verdicts] + out.append((_WASMQ_UNCHECKED_NAME, _wasmq_unchecked_job(), list(_WASMQ_FORM))) + return out + + +def write_wasmq_cases(args): + """--wasmq-cases OUT.jsonl [ES_NUMBER_FILE]: write wasmq_case_lines(), one JSON object a + line, {"name", "raw" (base64 of the job's bytes), "reasons"}. The ES_NUMBER_FILE the + self-test takes is accepted and not read: no wasmQ case uses it.""" + import base64 + if not 1 <= len(args) <= 2: + return _usage() + lines = wasmq_case_lines() + try: + with open(args[0], "w", encoding="ascii", newline="\n") as out: + for name, raw, want in lines: + out.write(json.dumps({"name": name, "raw": base64.b64encode(raw).decode("ascii"), + "reasons": want}) + "\n") + except (OSError, ValueError) as e: + sys.stderr.write("receipt_verify: cannot write %s (%s)\n" % (args[0], type(e).__name__)) + return 1 + sys.stdout.write("receipt_verify: wrote %d wasmQ cases to %s\n" % (len(lines), args[0])) + return 0 + + def self_test(args): results = [] @@ -2012,6 +3637,57 @@ def self_test(args): check("set: no receipt, alone", reasons_of(json.dumps( {"request": {}, "result": {"format": SET_FORMAT, "items": 5}}).encode()) == ["no_receipt"]) + # quantum-wasmq §6.5: the wasmQ jobs of _wasmq_tables, each judged once: its reasons, the + # rules it breaks, and its statement's problems, one entry a rule, in order, there + # exactly when wasmq_form is reported (another job states nothing). Each rule from 2 to + # the last is broken alone by one case at least; rule 1 cannot be (see the table). + def rules_of(raw): + job = read_json(raw) + return wasmq_broken(job["request"], job["result"]) + + def problem_rules(v): + return [int(p.split(":", 1)[0][len("rule "):]) for p in v["wasmq"]["problems"]] + + wasmq_cases, statement_cases, verdict_cases = _wasmq_tables() + unstated = [] + for name, raw_case, want, rules in wasmq_cases: + v = judge(raw_case) + check(name, v["reasons"] == want) + if rules is None: + held = "wasmq" not in v + else: + check(name + ": its rules", rules_of(raw_case) == rules) + held = ("wasmq" in v and problem_rules(v) == rules + and all(p.startswith("rule ") for p in v["wasmq"]["problems"]) + and ("wasmq_form" in v["reasons"]) == bool(rules)) + if not held: + unstated.append(name) + alone = set(rules[0] for _, _, want, rules in wasmq_cases if want == ["wasmq_form"] and len(rules) == 1) + # Rule 27 is never broken alone: a lifecycle fault at shot 0 has shots_run 0 (rule 18), so + # rule 32 holds the callouts to 0 (and rule 15 to a count) as rule 27 does. + check("wasmq: each of rules 2 to %d but 27 broken alone" % WASMQ_RULES, + alone == set(range(2, WASMQ_RULES + 1)) - {27}) + check("wasmq statement: its problems are the rules broken, in all %d cases%s" % ( + len(wasmq_cases), (": " + ", ".join(unstated)) if unstated else ""), not unstated) + exported = [name for name, _, _ in wasmq_case_lines()] + check("wasmq: the %d cases --wasmq-cases writes have distinct names" % len(exported), + len(set(exported)) == len(exported)) + for name, raw_case, want, alone_verdict in verdict_cases: + v = judge(raw_case) + check(name, v == _verdict(want) if alone_verdict else v["reasons"] == want) + for name, raw_case, want, want_statement in statement_cases: + v = judge(raw_case) + check("wasmq statement: " + name, v["reasons"] == want and v.get("wasmq") == want_statement) + check("wasmq statement: method provider states its provider too", + "provider" in judge(_wasmq_job(result={"method": "provider"}))) + check("wasmq statement: the first 64 problems, then how many more", + _first_and_more(["p%d" % i for i in range(70)]) == ["p%d" % i for i in range(64)] + ["and 6 more"] + and _first_and_more(["p"] * 64) == ["p"] * 64 and _first_and_more([]) == []) + wasmq_asked, wasmq_made = _wasmq_parts(False) + check("wasmq statement: a set holding a wasmQ job states none", "wasmq" not in judge(_set_body( + _WASMQ_SEED, {"programs": [wasmq_asked]}, [wasmq_asked], item_result=_dump(wasmq_made)))) + check("wasmq statement: another job states none", "wasmq" not in judge(body)) + # The scan of sent.settings against sent.fields is linear. Every key names the last # field, so a scan of the list walks all of it each time (tens of seconds). many_fields = ["f%06d" % i for i in range(50000)] @@ -2043,11 +3719,31 @@ def self_test(args): ("no-canonical-spelling", seeded("1152921504606847000"), [], ["inexact_number"]), ("set-seal-only", set_of(2, head_extra={"count": 3}), ["set_items"], []), ("names-layout-only", names_short, ["provider_members"], []), + ("wasmq-unchecked", _wasmq_unchecked_job(), ["wasmq_form"], []), ) for plant, raw, strict, lenient in plant_cases: check("plant %s: strict verdict" % plant, reasons_of(raw) == strict) check("plant %s: planted verdict" % plant, planted(plant, raw) == lenient) check("every plant is tested", set(c[0] for c in plant_cases) == set(PLANTS_KNOWN)) + # wasmq-unchecked runs no rule, so it states no problem; outcome and notes are no + # rules, and it still states them. + unchecked = _wasmq_unchecked_job() + PLANTS.add("wasmq-unchecked") + try: + unchecked_statement = judge(unchecked).get("wasmq") + finally: + PLANTS.discard("wasmq-unchecked") + check("plant wasmq-unchecked: strict statement", + judge(unchecked).get("wasmq") == _wasmq_statement("ok", ["rule 11: " + WASMQ_RULE_WORDS[11]])) + check("plant wasmq-unchecked: planted statement, no problem, its notes kept", + unchecked_statement == _wasmq_statement("ok")) + PLANTS.add("wasmq-unchecked") + try: + unchecked_notes = judge(_wasmq_job(fault=True)).get("wasmq") + finally: + PLANTS.discard("wasmq-unchecked") + check("plant wasmq-unchecked: notes still given", unchecked_notes == _wasmq_statement( + "fault", notes=['the host\'s unsealed status says "done"' + _WASMQ_SEALED_LINE + '"fault"'])) # Its decoding still refuses the alias, with the small-order rule off. check("plant lax-small-order-r: y = p + 1 still refused", planted("lax-small-order-r", alias_r_body) == ["bad_signature"]) diff --git a/wai/quantum-conformance/tools/wasmq_mutants.py b/wai/quantum-conformance/tools/wasmq_mutants.py new file mode 100644 index 000000000..62bfa6340 --- /dev/null +++ b/wai/quantum-conformance/tools/wasmq_mutants.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +"""wasmq_mutants.py: each rule of a wasmQ job's form (quantum-wasmq section 6.5), weakened in +each verifier, and held to the receipts differential: every weakening must be caught. + + python3 -I wasmq_mutants.py python --reference PATH/wai-quantum [--jobs N] [--rule N] + python3 -I wasmq_mutants.py rust-schemata ENGINE_RS OUT.json + python3 -I wasmq_mutants.py rust --reference PATH/wai-quantum --mutants OUT.json [--jobs N] + +The weakenings are made from the code, not written by hand: at each place a rule is checked, + + drop the check never fails; + bound a comparison moved by one (`<` for `<=`, `>` for `>=`, and back), or a whole + number in it one more; + type a count read as any number (0.5, -1, a number past 2^53), a digest as any string of + its length, two values compared as written rather than as JSON values; + null a member that is `null` read as absent; + absent a member that is absent read as `null`; + extreme the most of many read as the least, and back (a `max` over a list for a `min`). + +Each is assigned the rule whose check it is (the second verifier numbers its checks; the +engine marks each rule's code `// Rule N.` or `// N:`). The second verifier's mutants are +whole files; the engine's are one build (`rust-schemata` writes every mutant into the engine +behind the environment variable WQ_MUTANT, each off unless it is named), run once a mutant. + +A mutant is caught when `receipt_differential.py --wasmq-only --first` fails with it: the +corpus, the rules stage, the labelled cases and the one-member changes. A mutant no input +can tell from the text (one that only changes how a verdict already refused is worded, say) +is listed in EQUIVALENT with the reason; any other survivor fails the run. Counts are +reported per rule. Standard library only, Python 3.9+. +""" +import argparse +import ast +import copy +import json +import os +import re +import subprocess +import sys +import tempfile +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor + +HERE = os.path.dirname(os.path.abspath(__file__)) +SECOND = os.path.join(HERE, "receipt_verify.py") +DIFFERENTIAL = os.path.join(HERE, "receipt_differential.py") + +# Mutants that no input can tell from the text, each by its rule, kind and what it does +# ("RULES|KIND|WHAT"), with the reason no input tells it from the text. +EQUIVALENT = { + # The engine's. + '6|bound|whole\'s upper bound one more in `!m["name"].is_string() || !is_digest(&m["blake3"]) || !whole(&m["bytes"], 1, MAX_EXACT)`': "a count is at most 2^53 - 1 by its own reading (count), so a bound one past that bounds nothing more", + '6|bound|whole\'s upper bound one more in `!pr["name"].is_string() || !is_digest(&pr["blake3"]) || !whole(&pr["bytes"], 0, MAX_EXACT)`': "the same, for a prior's bytes", + '8|bound|whole\'s upper bound one more in `!whole(&run["seed"], 0, MAX_EXACT)`': "the same, for the seed", + "14|drop|the check never fails: `all.is_none()`": "rule 14's check of a call's largest against its fuel refuses a fuel that is no count already", + '16|type|same read loosely in `mods.len() != got.len() || mods.iter().zip(got).any(|(a, b)| !same(&a["blake3"], &b["blake3"]) || !same(&a["name"], &b["name"]))`#blake3': "a blake3 that is no string breaks rule 16's own check that the result's is a digest, so comparing it as written changes no rule", + '18|null|a null member read as absent: fuel["callouts"].get(name) in `fuel["callouts"].get(name)`': "both ways the stopped call is found missing are rule 18's", + '18|absent|an absent member read as null: fuel["callouts"].get(name) in `fuel["callouts"].get(name)`': "the same", + "19|type|count read loosely in `count(n).is_none_or(|n| n == 0)`": "rule 19's sum of the counts refuses a count that is no count already", + '20-35|null|a null member read as absent: r.get("fault") in `if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }`': "the fault is read only as an object: null and absent are both none", + '20-35|absent|an absent member read as null: r.get("fault") in `if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }`': "the same", + '35|null|a null member read as absent: r.get("fault") in `if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }`': "the same, where rule 35 reads the fault", + '35|absent|an absent member read as null: r.get("fault") in `if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }`': "the same", +} + + +def _equivalent(rules, kind, what): + return "%s|%s|%s" % (",".join(map(str, rules)), kind, what) in EQUIVALENT + +# ------------------------------------------------------------------ the second verifier + +# The type-check helpers: a value read through one of them is read the same whether it is +# absent or null (each refuses both), so a null-or-absent weakening under one changes nothing. +_CHECKERS = {"_count", "_count_in", "_text_is", "_lower_hex", "_obj", "_arr", "_or_zero", "_exactly", "isinstance", "_n"} +_LOOSE = {"_count": "_mut_count", "_count_in": "_mut_count_in", "_lower_hex": "_mut_lower_hex"} +_HELPERS_SOURCE = ''' + +def _mut_count(v): + """A weakening: any number is a count.""" + return isinstance(v, Num) and v.value >= -1e300 + + +def _mut_count_in(v, low, high): + return _mut_count(v) and low <= int(v.value) <= high + + +def _mut_lower_hex(v, n): + return isinstance(v, str) and len(v) == n + + +def _mut_get_null(v, name): + """A weakening: a member that is null is read as absent.""" + x = _get(v, name) + return _ABSENT if x is None else x + + +def _mut_get_absent(v, name): + """A weakening: a member that is absent is read as null.""" + x = _get(v, name) + return None if x is _ABSENT else x + + +def _mut_in_null(name, v): + return isinstance(v, dict) and v.get(name) is not None + + +def _mut_in_absent(name, v): + return isinstance(v, dict) +''' + + +def _wasmq_functions(tree): + """The second verifier's functions that check a wasmQ job's form: wasmq_broken and each + module-level function it calls that is no shared helper.""" + funcs = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)} + root = funcs["wasmq_broken"] + called = {n.func.id for n in ast.walk(root) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)} + shared = _CHECKERS | {"_get", "rule", "all", "any", "len", "set", "sorted", "sum", "min", "max", "abs", "int", "str", "list", "range", "isinstance", "_numbers", "_text_is"} + out = [root] + for name in sorted(called - shared): + f = funcs.get(name) + if f is not None and f.lineno > funcs.get("is_wasmq", root).lineno: + out.append(f) + return out + + +def _rule_calls(fn): + """(line, number, the call) for each rule(N, ...) in fn, in order.""" + out = [] + for n in ast.walk(fn): + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "rule" and n.args and isinstance(n.args[0], ast.Constant): + out.append((n.lineno, n.args[0].value, n)) + return sorted(out, key=lambda x: x[0]) + + +def _rule_of(node, root_calls, helper_rules, fn): + """The rule a node's check belongs to: the rule(N, ...) call around it; else, in + wasmq_broken, for a value assigned, every rule whose own code (from the rule before it to + its call) reads that name, or else the first rule called at or after its line; in a + helper, the rules whose checks call it.""" + for line, number, call in root_calls: + if any(x is node for x in ast.walk(call)): + return [number] + if fn.name != "wasmq_broken": + return helper_rules.get(fn.name, ["?"]) + top = next((st for st in fn.body if any(x is node for x in ast.walk(st))), None) + if isinstance(top, ast.Assign): + names = {t.id for target in top.targets for t in ast.walk(target) if isinstance(t, ast.Name)} + readers = [] + bounds = [0] + [line for line, _, _ in root_calls] + for (line, number, _), start in zip(root_calls, bounds): + block = [st for st in fn.body if start < st.lineno <= line and st is not top] + if any(isinstance(x, ast.Name) and x.id in names for st in block for x in ast.walk(st)): + readers.append(number) + if readers: + return sorted(set(readers)) + after = [number for line, number, _ in root_calls if line >= node.lineno] + return after[:1] or ["?"] + + +def python_mutants(path=SECOND): + """Every weakening of the second verifier's form check: (id, rules, what, the file's text).""" + with open(path, encoding="utf-8") as f: + source = f.read() + tree = ast.parse(source) + fns = _wasmq_functions(tree) + root = fns[0] + root_calls = _rule_calls(root) + helper_rules = defaultdict(list) + for line, number, call in root_calls: + for n in ast.walk(call): + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and any(n.func.id == f.name for f in fns[1:]): + helper_rules[n.func.id].append(number) + # A helper called from code before a rule(N, ...) is that rule's. + for f in fns[1:]: + if f.name not in helper_rules: + for n in ast.walk(root): + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == f.name: + helper_rules[f.name] = _rule_of(n, root_calls, {}, root) + + parents = {} + for fn in fns: + for p in ast.walk(fn): + for c in ast.iter_child_nodes(p): + parents[c] = p + sites = [] # (node, kind, what, transform) + + flip = {ast.Lt: ast.LtE, ast.LtE: ast.Lt, ast.Gt: ast.GtE, ast.GtE: ast.Gt} + for fn in fns: + for n in ast.walk(fn): + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "rule" and n.args and isinstance(n.args[0], ast.Constant): + sites.append((n, "drop", "rule %s never fails" % n.args[0].value, lambda m: ast.Call(func=m.func, args=[m.args[0], ast.Constant(True)], keywords=[]))) + if isinstance(n, ast.Compare): + for i, op in enumerate(n.ops): + if type(op) in flip: + def t(m, i=i): + m.ops[i] = flip[type(m.ops[i])]() + return m + sites.append((n, "bound", "%s for %s in %s" % (flip[type(op)].__name__, type(op).__name__, ast.unparse(n)), t)) + for c in ast.walk(n): + if isinstance(c, ast.Constant) and type(c.value) is int: + sites.append((c, "bound", "%d for %d in %s" % (c.value + 1, c.value, ast.unparse(n)), lambda m: ast.Constant(m.value + 1))) + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id in ("max", "min") and len(n.args) == 1: + other = "min" if n.func.id == "max" else "max" + sites.append((n, "extreme", "%s for %s in %s" % (other, n.func.id, ast.unparse(n)), lambda m, other=other: ast.Call(func=ast.Name(other, ast.Load()), args=m.args, keywords=m.keywords))) + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name): + if n.func.id == "_count_in": + for c in n.args[1:]: + if isinstance(c, ast.Constant) and type(c.value) is int: + sites.append((c, "bound", "%d for %d in %s" % (c.value + 1, c.value, ast.unparse(n)), lambda m: ast.Constant(m.value + 1))) + if n.func.id in _LOOSE: + sites.append((n, "type", "%s read loosely" % ast.unparse(n), lambda m: ast.Call(func=ast.Name(_LOOSE[m.func.id], ast.Load()), args=m.args, keywords=m.keywords))) + if n.func.id == "_get": + p = parents.get(n) + checked = isinstance(p, ast.Call) and isinstance(p.func, ast.Name) and p.func.id in _CHECKERS + if not checked: + sites.append((n, "null", "a null member read as absent: %s" % ast.unparse(n), lambda m: ast.Call(func=ast.Name("_mut_get_null", ast.Load()), args=m.args, keywords=[]))) + sites.append((n, "absent", "an absent member read as null: %s" % ast.unparse(n), lambda m: ast.Call(func=ast.Name("_mut_get_absent", ast.Load()), args=m.args, keywords=[]))) + if isinstance(n, ast.Compare) and len(n.ops) == 1 and isinstance(n.ops[0], (ast.In, ast.NotIn)) and isinstance(n.left, ast.Constant) and isinstance(n.left.value, str): + for kind, helper in (("null", "_mut_in_null"), ("absent", "_mut_in_absent")): + def t(m, helper=helper): + call = ast.Call(func=ast.Name(helper, ast.Load()), args=[m.left, m.comparators[0]], keywords=[]) + return ast.UnaryOp(ast.Not(), call) if isinstance(m.ops[0], ast.NotIn) else call + sites.append((n, kind, "`%s` in: %s" % (n.left.value, "null read as absent" if kind == "null" else "absent read as null"), t)) + + out = [] + for k, (node, kind, what, transform) in enumerate(sites): + fn = next(f for f in fns if any(x is node for x in ast.walk(f))) + rules = _rule_of(node, root_calls, helper_rules, fn) + mutant_tree = copy.deepcopy(tree) + # The same node in the copy: by its place in a walk of the same function. + index = [i for i, x in enumerate(ast.walk(fn)) if x is node][0] + mfn = next(f for f in mutant_tree.body if isinstance(f, ast.FunctionDef) and f.name == fn.name) + target = list(ast.walk(mfn))[index] + replaced = transform(copy.deepcopy(target)) + _replace(mfn, target, replaced) + ast.fix_missing_locations(mutant_tree) + text = ast.unparse(mutant_tree) + _HELPERS_SOURCE + ident = "P%04d %s:%d %s" % (k, fn.name, node.lineno, kind) + out.append((ident, rules, what, text)) + return out + + +def _replace(tree, old, new): + for p in ast.walk(tree): + for field, value in ast.iter_fields(p): + if value is old: + setattr(p, field, new) + return + if isinstance(value, list): + for i, x in enumerate(value): + if x is old: + value[i] = new + return + raise ValueError("no place to replace") + + +# ------------------------------------------------------------------ the engine + +_FUNCS = ("fn contract_form(", "pub fn problems(", "fn across(", "fn members(", "pub fn envelope_bytes(", "pub fn bare_transcript(") +_MARK = re.compile(r"^\s*// (?:Rule )?(\d+)[.:]") +_CMP = re.compile(r"(?<=[\w\)\]]) (>=|<=|>|<) (?=[\w\(&!*-])") +_FLIP = {">=": ">", ">": ">=", "<=": "<", "<": "<="} +_HELPERS_RS = ''' + +/// The weakening this build is, when wasmq_mutants.py made it: none otherwise. +fn mutant(k: u32) -> bool { + static WHICH: std::sync::OnceLock = std::sync::OnceLock::new(); + *WHICH.get_or_init(|| std::env::var("WQ_MUTANT").ok().and_then(|v| v.parse().ok()).unwrap_or(u32::MAX)) == k +} + +/// A weakening: any number is a count. +fn loose_count(v: &Value) -> Option { + v.as_f64().map(|x| x.abs() as u64) +} + +fn loose_whole(v: &Value, lo: u64, hi: u64) -> bool { + loose_count(v).is_some_and(|n| lo <= n && n <= hi) +} + +fn loose_digest(v: &Value) -> bool { + v.as_str().is_some_and(|s| s.len() == 64) +} + +fn raw_same(a: &Value, b: &Value) -> bool { + a == b +} +''' + + +def _calls(expr, name): + """Each place `name(` is called in expr: (start, end) of the call, its parentheses matched.""" + out = [] + for m in re.finditer(r"(? 0: + out.append(("bound", "whole's lower bound one less", expr[:a] + "whole(%s, %d, %s)" % (args[0], int(args[1]) - 1, args[2]) + expr[b:])) + out.append(("type", "whole read loosely", expr[:a] + "loose_whole(" + expr[a + len("whole("):])) + for name, loose in (("count", "loose_count"), ("is_digest", "loose_digest"), ("same", "raw_same")): + for a, b in _calls(expr, name): + out.append(("type", "%s read loosely" % name, expr[:a] + loose + expr[a + len(name):])) + for m in re.finditer(r"(?= 0 else text + _HELPERS_RS + with open(engine_rs, "w", encoding="utf-8") as f: + f.write(text) + with open(out_json, "w", encoding="utf-8") as f: + json.dump(mutants, f, indent=1) + print("rust-schemata: %d weakenings written into %s" % (len(mutants), engine_rs)) + + +def _mutate_line(line, following, rule, no, mutants): + """The line with each weakening of its check behind mutant(K).""" + def chain(expr, extra=()): + variants = list(extra) + _expr_mutants(expr) + if not variants: + return None + arms = [] + seen = defaultdict(int) + for kind, what, mut in variants: + k = len(mutants) + seen[what] += 1 + if seen[what] > 1 or sum(1 for _, w, _ in variants if w == what) > 1: + # The same weakening at two places of one expression: named by what it weakens. + at = re.search(r'&\w\["(\w+)"\], &\w\["\1"\]', mut[mut.find("raw_same(") if "raw_same(" in mut else 0:]) + what = "%s#%s" % (what, at.group(1) if at else seen[what]) + mutants.append({"id": k, "rule": rule, "kind": kind, "what": what, "line": no}) + arms.append("if mutant(%d) { %s }" % (k, mut)) + if kind == "drop": + mutants[-1]["what"] = "the check never fails: `%s`" % expr + return "(" + " else ".join(arms) + " else { %s })" % expr + pushes = following.strip().startswith(("out.push(", "problem(")) + m = re.match(r"^(\s*)if let (.+) = (.+) \{$", line) + if m and not pushes and re.search(r"\.(max|min)\(\)", m.group(3)): + # What a check after it reads, found as the most or the least of many. + c = chain(m.group(3)) + return "%sif let %s = %s {" % (m.group(1), m.group(2), c) if c else line + if m and pushes: + # A check of what is found, as rule 1's: never found. + k = len(mutants) + mutants.append({"id": k, "rule": rule, "kind": "drop", "what": "the check never fails: `%s`" % m.group(3), "line": no}) + return "%sif let %s = (%s).filter(|_| !mutant(%d)) {" % (m.group(1), m.group(2), m.group(3), k) + m = re.match(r"^(\s*)&& (.+)$", line) + if m and following.strip() == "{": + c = chain(m.group(2), [("drop", "the check never fails", "false")]) + return "%s&& %s" % (m.group(1), c) if c else line + m = re.match(r"^(\s*)if (?!let )(.+) \{$", line) + if m and pushes and " let " not in m.group(2): + c = chain(m.group(2), [("drop", "the check never fails", "false")]) + return "%sif %s {" % (m.group(1), c) if c else line + m = re.match(r"^(\s*)((?:Some\(.*?\)|None)) if (.+?) => (.*)$", line) + if m: + c = chain(m.group(3), [("drop", "the check never fails", "false")]) + return "%s%s if %s => %s" % (m.group(1), m.group(2), c, m.group(4)) if c else line + m = re.match(r"^(\s*)match (.+) \{$", line) + if m and ".get(" in m.group(2): + c = chain(m.group(2)) + return "%smatch %s {" % (m.group(1), c) if c else line + m = re.match(r"^(\s*)let (mut )?([\w(), ]+?)(: [^=]+)? = (.+);$", line) + if m and not m.group(5).lstrip().startswith(("|", "move |")) and any(x in m.group(5) for x in ("count(", "whole(", ".get(", " > ", " < ", " >= ", " <= ", "Some(")): + c = chain(m.group(5)) + return "%slet %s%s%s = %s;" % (m.group(1), m.group(2) or "", m.group(3), m.group(4) or "", c) if c else line + return line + + +# ------------------------------------------------------------------ running them + +def caught(env_extra, reference, second=None): + cmd = [sys.executable, "-I", DIFFERENTIAL, "--reference", reference, "--wasmq-only", "--first"] + if second: + cmd += ["--second", second] + env = dict(os.environ) + env.update(env_extra) + p = subprocess.run(cmd, capture_output=True, text=True, env=env) + first = next((l for l in p.stdout.splitlines() if l.startswith("DISAGREE")), "") + if p.returncode not in (0, 1): + return None, (p.stderr.strip().splitlines() or ["status %d" % p.returncode])[-1] + return p.returncode == 1, first + + +def report(results): + """results: (id, rules, kind, what, caught, why). Prints counts per rule; returns the + survivors not listed as equivalent.""" + per = defaultdict(lambda: defaultdict(int)) + survivors = [] + for ident, rules, kind, what, got, why in results: + for r in rules: + per[str(r)]["generated"] += 1 + per[str(r)]["caught" if got else "equivalent" if _equivalent(rules, kind, what) else "survived"] += 1 + if not got and not _equivalent(rules, kind, what): + survivors.append((ident, rules, kind, what, why)) + def key(r): + return (0, int(r)) if r.isdigit() else (1, r) + print("%-10s %9s %7s %10s %8s" % ("rule", "generated", "caught", "equivalent", "survived")) + for r in sorted(per, key=key): + c = per[r] + print("%-10s %9d %7d %10d %8d" % (r, c["generated"], c["caught"], c["equivalent"], c["survived"])) + for s in survivors: + print("SURVIVED %s (rules %s) %s: %s %s" % (s[0], ",".join(map(str, s[1])), s[2], s[3], s[4])) + return survivors + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("what", choices=["python", "rust", "rust-schemata"]) + ap.add_argument("paths", nargs="*") + ap.add_argument("--reference") + ap.add_argument("--mutants") + ap.add_argument("--jobs", type=int, default=4) + ap.add_argument("--rule", help="only the weakenings of this rule") + ap.add_argument("--list", action="store_true", help="list the weakenings, run none") + a = ap.parse_args() + if a.what == "rust-schemata": + engine_rs, out_json = a.paths + return rust_schemata(engine_rs, out_json) + if a.what == "python" and a.list: + ms = [m for m in python_mutants() if not a.rule or a.rule in map(str, m[1])] + counts = defaultdict(lambda: defaultdict(int)) + for ident, rules, what, _ in ms: + print(ident, rules, what) + for r in rules: + counts[str(r)][ident.split()[-1]] += 1 + for r in sorted(counts, key=lambda r: (0, int(r)) if r.isdigit() else (1, r)): + print("rule %s: %s" % (r, dict(counts[r]))) + print("%d weakenings" % len(ms)) + return 0 + if not a.reference: + ap.error("--reference is required") + cache = tempfile.mkdtemp(prefix="wasmq-mutants-cache-") + base = {"RD_CACHE": cache} + # The unweakened pair agrees: else nothing below means anything. + ok, why = caught(base, a.reference) + if ok is not False: + print("the unweakened verifiers do not agree, or the differential failed: %s" % why) + return 2 + results = [] + if a.what == "python": + ms = [m for m in python_mutants() if not a.rule or a.rule in map(str, m[1])] + if a.list: + for ident, rules, what, _ in ms: + print(ident, rules, what) + return 0 + tmp = tempfile.mkdtemp(prefix="wasmq-mutants-py-") + + def one(m): + ident, rules, what, text = m + path = os.path.join(tmp, "m%s.py" % ident.split()[0]) + with open(path, "w", encoding="utf-8") as f: + f.write(text) + got, why = caught(base, a.reference, second=path) + os.remove(path) + return (ident, rules, ident.split()[-1], what, bool(got), why) + with ThreadPoolExecutor(a.jobs) as pool: + for r in pool.map(one, ms): + results.append(r) + print("%s %-8s %s %s" % ("caught " if r[4] else "SURVIVED", r[2], r[0], r[3]), flush=True) + else: + with open(a.mutants, encoding="utf-8") as f: + ms = [m for m in json.load(f) if not a.rule or m["rule"] == a.rule] + + def one(m): + env = dict(base, WQ_MUTANT=str(m["id"])) + got, why = caught(env, a.reference) + ident = "R%04d line %d %s" % (m["id"], m["line"], m["kind"]) + return (ident, [m["rule"]], m["kind"], m["what"], bool(got), why) + with ThreadPoolExecutor(a.jobs) as pool: + for r in pool.map(one, ms): + results.append(r) + print("%s %-8s %s %s" % ("caught " if r[4] else "SURVIVED", r[2], r[0], r[3]), flush=True) + survivors = report(results) + print("%s: %d weakenings, %d caught, %d survived" % (a.what, len(results), sum(1 for r in results if r[4]), len(survivors))) + return 1 if survivors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wai/quantum-conformance/wasmq/.gitattributes b/wai/quantum-conformance/wasmq/.gitattributes new file mode 100644 index 000000000..f83aae71c --- /dev/null +++ b/wai/quantum-conformance/wasmq/.gitattributes @@ -0,0 +1,2 @@ +# The modules of record, the envelopes and the WebAssembly vectors are compared byte for byte: never converted. +* -text diff --git a/wai/quantum-conformance/wasmq/README.md b/wai/quantum-conformance/wasmq/README.md new file mode 100644 index 000000000..a47b4e963 --- /dev/null +++ b/wai/quantum-conformance/wasmq/README.md @@ -0,0 +1,27 @@ +# wasmQ conformance + +Vectors for [`../../extensions/quantum-wasmq.md`](../../extensions/quantum-wasmq.md) §9. + +**Provenance.** Generated here by `wai-wasmq`'s `tests/vectors.rs` (`WASMQ_BLESS=1`) from the +reference programs (`wai-wasmq/src/reference.rs`) and modules written as WebAssembly text or +bytes in that file, which also holds every committed file to the bytes it makes. No media. + +| path | what | +|---|---| +| `modules/` | the reference modules of record, built on x86_64 Linux by `wai-wasmq/modules/build.sh`, with `modules.json`: their digests and the SHA-256 of every source that made them | +| `envelopes/` | envelopes; `vectors.json` `runs` pins what each gives on every engine and host: status, fault, fuel, the transcript's digest, the result's digest, the receipt's digest and, under the vectors' key (32 bytes of 7), the whole job's digest | +| `envelopes/surface3-decode64.*` | one run in the open: its program, the prior its decoder reads (the code's parity checks) and its sealed job | +| `refuse/` | modules (also as WebAssembly text, where they have one) and envelopes a runner refuses; `vectors.json` `refuse` gives each one's code. There is at least one for every code, and each is one defect away from a module or an envelope that is accepted, which the test that makes them checks | + +```sh +cargo run --release --manifest-path wai/wai-wasmq/Cargo.toml --bin wasmq_vectors -- verify wai/quantum-conformance/wasmq +``` + +prints a line for each vector and a digest of all of them: the same on every host and +every engine that conforms. Under wasm32 it runs with the interpreter alone: + +```sh +cargo build --release --target wasm32-wasip2 --manifest-path wai/wai-wasmq/Cargo.toml --no-default-features --bin wasmq_vectors +wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir wai/quantum-conformance/wasmq::/v \ + wai/wai-wasmq/target/wasm32-wasip2/release/wasmq_vectors.wasm verify /v +``` diff --git a/wai/quantum-conformance/wasmq/envelopes/atoms4x4-plan64.wai b/wai/quantum-conformance/wasmq/envelopes/atoms4x4-plan64.wai new file mode 100644 index 000000000..c19d3a20c Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/atoms4x4-plan64.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/atoms9x9-plan.wai b/wai/quantum-conformance/wasmq/envelopes/atoms9x9-plan.wai new file mode 100644 index 000000000..70bdede08 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/atoms9x9-plan.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/bits-nothing-writes.wai b/wai/quantum-conformance/wasmq/envelopes/bits-nothing-writes.wai new file mode 100644 index 000000000..8461ab005 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/bits-nothing-writes.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/block-bit-not-in-outcome.wai b/wai/quantum-conformance/wasmq/envelopes/block-bit-not-in-outcome.wai new file mode 100644 index 000000000..897cdc966 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/block-bit-not-in-outcome.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-bad-result.wai b/wai/quantum-conformance/wasmq/envelopes/fault-bad-result.wai new file mode 100644 index 000000000..e0bc7889f Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-bad-result.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-fuel-instance.wai b/wai/quantum-conformance/wasmq/envelopes/fault-fuel-instance.wai new file mode 100644 index 000000000..10e93d5be Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-fuel-instance.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-fuel-run.wai b/wai/quantum-conformance/wasmq/envelopes/fault-fuel-run.wai new file mode 100644 index 000000000..43b71cd0d Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-fuel-run.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-fuel.wai b/wai/quantum-conformance/wasmq/envelopes/fault-fuel.wai new file mode 100644 index 000000000..8a990e786 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-fuel.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-in-init.wai b/wai/quantum-conformance/wasmq/envelopes/fault-in-init.wai new file mode 100644 index 000000000..bdcd3383d Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-in-init.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-in-prior.wai b/wai/quantum-conformance/wasmq/envelopes/fault-in-prior.wai new file mode 100644 index 000000000..9259e6a0e Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-in-prior.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-in-seed.wai b/wai/quantum-conformance/wasmq/envelopes/fault-in-seed.wai new file mode 100644 index 000000000..353695bc6 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-in-seed.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-null.wai b/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-null.wai new file mode 100644 index 000000000..6fc883e7b Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-null.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-type.wai b/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-type.wai new file mode 100644 index 000000000..ac741f2f7 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-type.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-integer-overflow.wai b/wai/quantum-conformance/wasmq/envelopes/fault-integer-overflow.wai new file mode 100644 index 000000000..2adcf2bba Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-integer-overflow.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-invalid-conversion.wai b/wai/quantum-conformance/wasmq/envelopes/fault-invalid-conversion.wai new file mode 100644 index 000000000..e07a8375d Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-invalid-conversion.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-list-length.wai b/wai/quantum-conformance/wasmq/envelopes/fault-list-length.wai new file mode 100644 index 000000000..f056557ce Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-list-length.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-list-misaligned.wai b/wai/quantum-conformance/wasmq/envelopes/fault-list-misaligned.wai new file mode 100644 index 000000000..54dedcda7 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-list-misaligned.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-list-outside.wai b/wai/quantum-conformance/wasmq/envelopes/fault-list-outside.wai new file mode 100644 index 000000000..31ba83d26 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-list-outside.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-list-past-its-bits.wai b/wai/quantum-conformance/wasmq/envelopes/fault-list-past-its-bits.wai new file mode 100644 index 000000000..6f241c7f7 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-list-past-its-bits.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-memory-out-of-bounds.wai b/wai/quantum-conformance/wasmq/envelopes/fault-memory-out-of-bounds.wai new file mode 100644 index 000000000..14d8ceb24 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-memory-out-of-bounds.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai b/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai new file mode 100644 index 000000000..8f2b03be5 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-table-out-of-bounds.wai b/wai/quantum-conformance/wasmq/envelopes/fault-table-out-of-bounds.wai new file mode 100644 index 000000000..91d14eb07 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-table-out-of-bounds.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-trap.wai b/wai/quantum-conformance/wasmq/envelopes/fault-trap.wai new file mode 100644 index 000000000..56508a134 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-trap.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-unreachable.wai b/wai/quantum-conformance/wasmq/envelopes/fault-unreachable.wai new file mode 100644 index 000000000..4874640e5 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/fault-unreachable.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/gross-decode.wai b/wai/quantum-conformance/wasmq/envelopes/gross-decode.wai new file mode 100644 index 000000000..f62eb5eb2 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/gross-decode.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai b/wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai new file mode 100644 index 000000000..6e4a58066 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/list-echo.wai b/wai/quantum-conformance/wasmq/envelopes/list-echo.wai new file mode 100644 index 000000000..bb221a40b Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/list-echo.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/list-post.wai b/wai/quantum-conformance/wasmq/envelopes/list-post.wai new file mode 100644 index 000000000..90e4b5597 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/list-post.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/measures-nothing.wai b/wai/quantum-conformance/wasmq/envelopes/measures-nothing.wai new file mode 100644 index 000000000..29bbff584 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/measures-nothing.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/nan-canonical.wai b/wai/quantum-conformance/wasmq/envelopes/nan-canonical.wai new file mode 100644 index 000000000..f3d3f8bd1 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/nan-canonical.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/physical-qubits.wai b/wai/quantum-conformance/wasmq/envelopes/physical-qubits.wai new file mode 100644 index 000000000..47f0bdc13 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/physical-qubits.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/reset-draws.wai b/wai/quantum-conformance/wasmq/envelopes/reset-draws.wai new file mode 100644 index 000000000..43190a265 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/reset-draws.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/signed-argument.wai b/wai/quantum-conformance/wasmq/envelopes/signed-argument.wai new file mode 100644 index 000000000..d280ca851 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/signed-argument.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.checks.bin b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.checks.bin new file mode 100644 index 000000000..c99e1d527 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.checks.bin differ diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json new file mode 100644 index 000000000..e1115be60 --- /dev/null +++ b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json @@ -0,0 +1,110 @@ +{ + "status": "done", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "decode64", + "extern": "decode64", + "module": 0 + } + ], + "modules": [ + { + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962", + "bytes": 39442, + "name": "qldpc-decoder", + "priors": [ + { + "blake3": "1f593bab90aa9ff3995361a32a9860760f020bec8759e6f94175c8b4b211f4db", + "bytes": 87, + "name": "checks" + } + ] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "72471b167c0b1b590f0d817a75d484451408a0448b9d9a8924b670cdef131d85", + "bytes": 1104, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1234, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "ok", + "method": "stabilizer", + "qubits": 17, + "shots": 32, + "shots_run": 32, + "seed": 1234, + "counts": { + "000000000000000000000000001001": 3, + "000000000000000000000000001010": 2, + "000000000000000000000000001011": 4, + "000000000000000000000000001101": 2, + "000000000000000000000000001110": 2, + "000000000000000000000000001111": 1, + "000000000000000000000100010000": 4, + "000000000000000000010010000010": 2, + "000000000000000000100001010011": 2, + "000000000000000001000000110100": 2, + "000000000000000010000010100101": 2, + "000000000000000100000001000110": 3, + "000000011000000000000100010001": 2, + "110000000000001000000000101000": 1 + }, + "callouts": 32, + "fuel": { + "used": 836380, + "instances": 248032, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "decode64": { + "calls": 32, + "fuel": 588348, + "max": 21751 + } + } + }, + "modules": [ + { + "name": "qldpc-decoder", + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962" + } + ], + "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", + "accuracy": { + "class": "exact" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", + "digest": "df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1", + "signature": "96f9a03e1a8b88d68cd1d2498fa1a3589556c9f628922eab3dd0284322e41de3640edbf71561c7fd265801a81c1250db5c7788b40ada40f42c8e6a773e7aa008" + } + } +} diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.qasm b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.qasm new file mode 100644 index 000000000..f170f9881 --- /dev/null +++ b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.qasm @@ -0,0 +1,61 @@ +OPENQASM 3.0; +include "stdgates.inc"; +// A memory round of a code of 9 qubits and 4 parity checks, decoded in the middle of the circuit. +extern decode64(bit[4]) -> bit[9]; +qubit[9] d; +qubit[4] a; +qubit[4] c0; +bit[4] e0; +bit[4] s; +bit[9] corr; +bit[4] s2; +bit[9] fin; +h c0; +e0 = measure c0; +if (e0 == 0) x d[0]; +if (e0 == 1) x d[1]; +if (e0 == 2) x d[2]; +if (e0 == 3) x d[3]; +if (e0 == 4) x d[4]; +if (e0 == 5) x d[5]; +if (e0 == 6) x d[6]; +if (e0 == 7) x d[7]; +if (e0 == 8) x d[8]; +cx d[0], a[0]; +cx d[1], a[0]; +cx d[3], a[0]; +cx d[4], a[0]; +cx d[4], a[1]; +cx d[5], a[1]; +cx d[7], a[1]; +cx d[8], a[1]; +cx d[3], a[2]; +cx d[6], a[2]; +cx d[2], a[3]; +cx d[5], a[3]; +s = measure a; +corr = decode64(s); +if (corr[0]) x d[0]; +if (corr[1]) x d[1]; +if (corr[2]) x d[2]; +if (corr[3]) x d[3]; +if (corr[4]) x d[4]; +if (corr[5]) x d[5]; +if (corr[6]) x d[6]; +if (corr[7]) x d[7]; +if (corr[8]) x d[8]; +reset a; +cx d[0], a[0]; +cx d[1], a[0]; +cx d[3], a[0]; +cx d[4], a[0]; +cx d[4], a[1]; +cx d[5], a[1]; +cx d[7], a[1]; +cx d[8], a[1]; +cx d[3], a[2]; +cx d[6], a[2]; +cx d[2], a[3]; +cx d[5], a[3]; +s2 = measure a; +fin = measure d; diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai new file mode 100644 index 000000000..b993941d4 Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai b/wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai new file mode 100644 index 000000000..e9584bd5f Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai differ diff --git a/wai/quantum-conformance/wasmq/envelopes/two-modules-in-order.wai b/wai/quantum-conformance/wasmq/envelopes/two-modules-in-order.wai new file mode 100644 index 000000000..75830183e Binary files /dev/null and b/wai/quantum-conformance/wasmq/envelopes/two-modules-in-order.wai differ diff --git a/wai/quantum-conformance/wasmq/modules/atom_planner.wasm b/wai/quantum-conformance/wasmq/modules/atom_planner.wasm new file mode 100644 index 000000000..be55193f3 Binary files /dev/null and b/wai/quantum-conformance/wasmq/modules/atom_planner.wasm differ diff --git a/wai/quantum-conformance/wasmq/modules/modules.json b/wai/quantum-conformance/wasmq/modules/modules.json new file mode 100644 index 000000000..d7799ca06 --- /dev/null +++ b/wai/quantum-conformance/wasmq/modules/modules.json @@ -0,0 +1,30 @@ +{ + "built_with": "rustc 1.99.1 (58cf8f9c3 2026-10-13)", + "host": "x86_64-unknown-linux-gnu", + "modules": { + "atom_planner": { + "bytes": 24165, + "file": "atom_planner.wasm", + "sha256": "58f9897762d0948b8e8bf15e0943c174780233e2d9425f93f3ae72614124270c" + }, + "qldpc_decoder": { + "bytes": 39442, + "file": "qldpc_decoder.wasm", + "sha256": "9af5a3a8ff6769f58ab22b7c15e4838239aa235abb0697ef22ef2cc5624f7a69" + } + }, + "sources_sha256": { + "rust-toolchain.toml": "cfd9dd2847ba07825df74b7ddd61f8cfa602f102cdfa3a783bb022c2474abc6f", + "wai/wai-quantum/src/quantum_atom.rs": "3dbe43489a6390883f03f349eff9cf29a43c6b27738bd654a37d319a7d571860", + "wai/wai-quantum/src/quantum_ops.rs": "8f0573bf539f5ed4bf65950f12336a7ba10d488385f41e41620ef2c0d2e1e63a", + "wai/wai-quantum/src/quantum_qec.rs": "db8a9afd91009a7de1ccfdffb52a08c67a4ecb9ccbdfa2627cd532c9cfdad71f", + "wai/wai-wasmq/modules/.cargo/config.toml": "717fd52352d25db091343ffa371a2939f47e6e2d6675d8da561ad16cdd148915", + "wai/wai-wasmq/modules/Cargo.lock": "47bd630b7da5eb32bdcc0f4bd34abd53e4711942f194465ee958645ec5500f6f", + "wai/wai-wasmq/modules/Cargo.toml": "4a193661600e5ccb524a1c69c1ab2fb507e0d63170bdc3253f6ad09222bccd55", + "wai/wai-wasmq/modules/atom-planner/Cargo.toml": "946fda9df9ca6c37ff46dc6b3712bd142560b6ff9793c801a2311dac7f7631cc", + "wai/wai-wasmq/modules/atom-planner/src/lib.rs": "ecad2c1bc3d6fff01174ccc5ed15d996688d98297583075245b7c5d1cb4bd5e9", + "wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml": "586a4660fd35e2f4ce6608f50cce978d171c7ca87cc152d53495f77420cb5b06", + "wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs": "e19afaf234b091b5ddc3c346ebf479f5c1bffddae78b071c77763c2f91b7adeb" + }, + "target": "wasm32-unknown-unknown" +} diff --git a/wai/quantum-conformance/wasmq/modules/qldpc_decoder.wasm b/wai/quantum-conformance/wasmq/modules/qldpc_decoder.wasm new file mode 100644 index 000000000..bc5f14cf9 Binary files /dev/null and b/wai/quantum-conformance/wasmq/modules/qldpc_decoder.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/atomic.wasm b/wai/quantum-conformance/wasmq/refuse/atomic.wasm new file mode 100644 index 000000000..e562c5f8a Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/atomic.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/atomic.wat b/wai/quantum-conformance/wasmq/refuse/atomic.wat new file mode 100644 index 000000000..1c4f8a249 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/atomic.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) i32.const 0 local.get 0 i64.atomic.rmw.add)) diff --git a/wai/quantum-conformance/wasmq/refuse/body-without-end.wasm b/wai/quantum-conformance/wasmq/refuse/body-without-end.wasm new file mode 100644 index 000000000..8a365415c Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/body-without-end.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai b/wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai new file mode 100644 index 000000000..fb996db41 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai b/wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai new file mode 100644 index 000000000..6ecdb5703 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai b/wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai new file mode 100644 index 000000000..42408a72f Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai b/wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai new file mode 100644 index 000000000..988b2c8f3 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai b/wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai new file mode 100644 index 000000000..1875564b5 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai b/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai new file mode 100644 index 000000000..a86a28bb1 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/data-past-memory.wasm b/wai/quantum-conformance/wasmq/refuse/data-past-memory.wasm new file mode 100644 index 000000000..d66ede046 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/data-past-memory.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/data-past-memory.wat b/wai/quantum-conformance/wasmq/refuse/data-past-memory.wat new file mode 100644 index 000000000..9b60a2364 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/data-past-memory.wat @@ -0,0 +1 @@ +(module (memory 1 1) (data (i32.const 65530) "0123456789") (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/frame-too-large.wasm b/wai/quantum-conformance/wasmq/refuse/frame-too-large.wasm new file mode 100644 index 000000000..a42447e41 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/frame-too-large.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/frame-too-large.wat b/wai/quantum-conformance/wasmq/refuse/frame-too-large.wat new file mode 100644 index 000000000..f5719e25a --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/frame-too-large.wat @@ -0,0 +1 @@ +(module (func (local i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 ))) diff --git a/wai/quantum-conformance/wasmq/refuse/function-of-no-type.wasm b/wai/quantum-conformance/wasmq/refuse/function-of-no-type.wasm new file mode 100644 index 000000000..4f3432c87 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/function-of-no-type.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/gate-outside-the-clifford-gates.wai b/wai/quantum-conformance/wasmq/refuse/gate-outside-the-clifford-gates.wai new file mode 100644 index 000000000..a25007851 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/gate-outside-the-clifford-gates.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/import-clock.wasm b/wai/quantum-conformance/wasmq/refuse/import-clock.wasm new file mode 100644 index 000000000..4acb8aef6 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/import-clock.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/import-clock.wat b/wai/quantum-conformance/wasmq/refuse/import-clock.wat new file mode 100644 index 000000000..1c90ebc81 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/import-clock.wat @@ -0,0 +1 @@ +(module (import "wasi:clocks/monotonic-clock" "now" (func (result i64))) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/import-random.wasm b/wai/quantum-conformance/wasmq/refuse/import-random.wasm new file mode 100644 index 000000000..bb8dd60fa Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/import-random.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/import-random.wat b/wai/quantum-conformance/wasmq/refuse/import-random.wat new file mode 100644 index 000000000..c7dd5b85e --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/import-random.wat @@ -0,0 +1 @@ +(module (import "wasi_snapshot_preview1" "random_get" (func (param i32 i32) (result i32))) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai b/wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai new file mode 100644 index 000000000..f1a605fa7 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/manifest-changed.wai b/wai/quantum-conformance/wasmq/refuse/manifest-changed.wai new file mode 100644 index 000000000..94efdf2b4 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/manifest-changed.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai b/wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai new file mode 100644 index 000000000..1e2e0aca1 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/memory-past-a-run-s-among-its-modules.wai b/wai/quantum-conformance/wasmq/refuse/memory-past-a-run-s-among-its-modules.wai new file mode 100644 index 000000000..ef5de280e Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/memory-past-a-run-s-among-its-modules.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/memory-too-large.wasm b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wasm new file mode 100644 index 000000000..511da9ceb Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat new file mode 100644 index 000000000..41d6c68ed --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat @@ -0,0 +1 @@ +(module (memory 1 4097) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wasm b/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wasm new file mode 100644 index 000000000..91e297755 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wat b/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wat new file mode 100644 index 000000000..a8364daa2 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/memory-unbounded.wat @@ -0,0 +1 @@ +(module (memory 1) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai b/wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai new file mode 100644 index 000000000..02b0e3f90 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/no-shots.wai b/wai/quantum-conformance/wasmq/refuse/no-shots.wai new file mode 100644 index 000000000..028746324 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/no-shots.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/other-capability.wai b/wai/quantum-conformance/wasmq/refuse/other-capability.wai new file mode 100644 index 000000000..17f9faf51 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/other-capability.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai b/wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai new file mode 100644 index 000000000..8350850b0 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai b/wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai new file mode 100644 index 000000000..4d9e4b59b Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai b/wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai new file mode 100644 index 000000000..713410b9a Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/program-hash-mismatch.wai b/wai/quantum-conformance/wasmq/refuse/program-hash-mismatch.wai new file mode 100644 index 000000000..22d7c5a80 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/program-hash-mismatch.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/program-measure-with-no-bit.wai b/wai/quantum-conformance/wasmq/refuse/program-measure-with-no-bit.wai new file mode 100644 index 000000000..fc9e1e15c Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/program-measure-with-no-bit.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai b/wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai new file mode 100644 index 000000000..de7125b8a Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/ref-null.wasm b/wai/quantum-conformance/wasmq/refuse/ref-null.wasm new file mode 100644 index 000000000..6ccc7ce6d Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/ref-null.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/ref-null.wat b/wai/quantum-conformance/wasmq/refuse/ref-null.wat new file mode 100644 index 000000000..0f5829dd2 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/ref-null.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) ref.null func ref.is_null i64.extend_i32_u)) diff --git a/wai/quantum-conformance/wasmq/refuse/reference-type.wasm b/wai/quantum-conformance/wasmq/refuse/reference-type.wasm new file mode 100644 index 000000000..0bb2fc816 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/reference-type.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/reference-type.wat b/wai/quantum-conformance/wasmq/refuse/reference-type.wat new file mode 100644 index 000000000..dabaf6826 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/reference-type.wat @@ -0,0 +1 @@ +(module (func (param externref)) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wasm b/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wasm new file mode 100644 index 000000000..4a5c3a093 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wat b/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wat new file mode 100644 index 000000000..09de3e97a --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat local.get 0 i64x2.splat local.get 0 i64x2.splat f32x4.relaxed_madd i64x2.extract_lane 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/reserved-export.wasm b/wai/quantum-conformance/wasmq/refuse/reserved-export.wasm new file mode 100644 index 000000000..60cb2a6a1 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/reserved-export.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/reserved-export.wat b/wai/quantum-conformance/wasmq/refuse/reserved-export.wat new file mode 100644 index 000000000..dc806d8e7 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/reserved-export.wat @@ -0,0 +1 @@ +(module (global (export "__wasmq_fuel") (mut i64) (i64.const 0)) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/results-past-what-a-host-holds.wai b/wai/quantum-conformance/wasmq/refuse/results-past-what-a-host-holds.wai new file mode 100644 index 000000000..81395d12f Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/results-past-what-a-host-holds.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/seed-a-double-cannot-hold.wai b/wai/quantum-conformance/wasmq/refuse/seed-a-double-cannot-hold.wai new file mode 100644 index 000000000..c330117c4 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/seed-a-double-cannot-hold.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wasm b/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wasm new file mode 100644 index 000000000..b43092367 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wat b/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wat new file mode 100644 index 000000000..e67bebb02 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wat @@ -0,0 +1 @@ +(module (func (export "wasmq-seed") (param i32)) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/shared-memory.wasm b/wai/quantum-conformance/wasmq/refuse/shared-memory.wasm new file mode 100644 index 000000000..380b09e57 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/shared-memory.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/shared-memory.wat b/wai/quantum-conformance/wasmq/refuse/shared-memory.wat new file mode 100644 index 000000000..87e43a543 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/shared-memory.wat @@ -0,0 +1 @@ +(module (memory 1 1 shared) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/simd.wasm b/wai/quantum-conformance/wasmq/refuse/simd.wasm new file mode 100644 index 000000000..96e9d20e7 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/simd.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/simd.wat b/wai/quantum-conformance/wasmq/refuse/simd.wat new file mode 100644 index 000000000..01582c310 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/simd.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat i64x2.extract_lane 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/start-function.wasm b/wai/quantum-conformance/wasmq/refuse/start-function.wasm new file mode 100644 index 000000000..021aab8a8 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/start-function.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/start-function.wat b/wai/quantum-conformance/wasmq/refuse/start-function.wat new file mode 100644 index 000000000..a041740db --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/start-function.wat @@ -0,0 +1 @@ +(module (func $s) (start $s) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/table-grow.wasm b/wai/quantum-conformance/wasmq/refuse/table-grow.wasm new file mode 100644 index 000000000..d5bc4532b Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/table-grow.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/table-grow.wat b/wai/quantum-conformance/wasmq/refuse/table-grow.wat new file mode 100644 index 000000000..a3e2dc7f1 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/table-grow.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) unreachable table.grow 0 i64.extend_i32_u)) diff --git a/wai/quantum-conformance/wasmq/refuse/table-size.wasm b/wai/quantum-conformance/wasmq/refuse/table-size.wasm new file mode 100644 index 000000000..69215ef83 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/table-size.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/table-size.wat b/wai/quantum-conformance/wasmq/refuse/table-size.wat new file mode 100644 index 000000000..851c26fac --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/table-size.wat @@ -0,0 +1 @@ +(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) table.size 0 i64.extend_i32_u)) diff --git a/wai/quantum-conformance/wasmq/refuse/tail-call.wasm b/wai/quantum-conformance/wasmq/refuse/tail-call.wasm new file mode 100644 index 000000000..f84e7c040 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/tail-call.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/tail-call.wat b/wai/quantum-conformance/wasmq/refuse/tail-call.wat new file mode 100644 index 000000000..4b2cae8db --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/tail-call.wat @@ -0,0 +1 @@ +(module (func $g (param i64) (result i64) local.get 0) (func (param i64) (result i64) local.get 0 return_call $g) (func (export "f") (param i64) (result i64) local.get 0)) diff --git a/wai/quantum-conformance/wasmq/refuse/too-many-exports.wasm b/wai/quantum-conformance/wasmq/refuse/too-many-exports.wasm new file mode 100644 index 000000000..f278f491c Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/too-many-exports.wasm differ diff --git a/wai/quantum-conformance/wasmq/refuse/too-many-exports.wat b/wai/quantum-conformance/wasmq/refuse/too-many-exports.wat new file mode 100644 index 000000000..4e9cf41e4 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/too-many-exports.wat @@ -0,0 +1 @@ +(module (func) (export "e0" (func 0)) (export "e1" (func 0)) (export "e2" (func 0)) (export "e3" (func 0)) (export "e4" (func 0)) (export "e5" (func 0)) (export "e6" (func 0)) (export "e7" (func 0)) (export "e8" (func 0)) (export "e9" (func 0)) (export "e10" (func 0)) (export "e11" (func 0)) (export "e12" (func 0)) (export "e13" (func 0)) (export "e14" (func 0)) (export "e15" (func 0)) (export "e16" (func 0)) (export "e17" (func 0)) (export "e18" (func 0)) (export "e19" (func 0)) (export "e20" (func 0)) (export "e21" (func 0)) (export "e22" (func 0)) (export "e23" (func 0)) (export "e24" (func 0)) (export "e25" (func 0)) (export "e26" (func 0)) (export "e27" (func 0)) (export "e28" (func 0)) (export "e29" (func 0)) (export "e30" (func 0)) (export "e31" (func 0)) (export "e32" (func 0)) (export "e33" (func 0)) (export "e34" (func 0)) (export "e35" (func 0)) (export "e36" (func 0)) (export "e37" (func 0)) (export "e38" (func 0)) (export "e39" (func 0)) (export "e40" (func 0)) (export "e41" (func 0)) (export "e42" (func 0)) (export "e43" (func 0)) (export "e44" (func 0)) (export "e45" (func 0)) (export "e46" (func 0)) (export "e47" (func 0)) (export "e48" (func 0)) (export "e49" (func 0)) (export "e50" (func 0)) (export "e51" (func 0)) (export "e52" (func 0)) (export "e53" (func 0)) (export "e54" (func 0)) (export "e55" (func 0)) (export "e56" (func 0)) (export "e57" (func 0)) (export "e58" (func 0)) (export "e59" (func 0)) (export "e60" (func 0)) (export "e61" (func 0)) (export "e62" (func 0)) (export "e63" (func 0)) (export "e64" (func 0)) (export "e65" (func 0)) (export "e66" (func 0)) (export "e67" (func 0)) (export "e68" (func 0)) (export "e69" (func 0)) (export "e70" (func 0)) (export "e71" (func 0)) (export "e72" (func 0)) (export "e73" (func 0)) (export "e74" (func 0)) (export "e75" (func 0)) (export "e76" (func 0)) (export "e77" (func 0)) (export "e78" (func 0)) (export "e79" (func 0)) (export "e80" (func 0)) (export "e81" (func 0)) (export "e82" (func 0)) (export "e83" (func 0)) (export "e84" (func 0)) (export "e85" (func 0)) (export "e86" (func 0)) (export "e87" (func 0)) (export "e88" (func 0)) (export "e89" (func 0)) (export "e90" (func 0)) (export "e91" (func 0)) (export "e92" (func 0)) (export "e93" (func 0)) (export "e94" (func 0)) (export "e95" (func 0)) (export "e96" (func 0)) (export "e97" (func 0)) (export "e98" (func 0)) (export "e99" (func 0)) (export "e100" (func 0)) (export "e101" (func 0)) (export "e102" (func 0)) (export "e103" (func 0)) (export "e104" (func 0)) (export "e105" (func 0)) (export "e106" (func 0)) (export "e107" (func 0)) (export "e108" (func 0)) (export "e109" (func 0)) (export "e110" (func 0)) (export "e111" (func 0)) (export "e112" (func 0)) (export "e113" (func 0)) (export "e114" (func 0)) (export "e115" (func 0)) (export "e116" (func 0)) (export "e117" (func 0)) (export "e118" (func 0)) (export "e119" (func 0)) (export "e120" (func 0)) (export "e121" (func 0)) (export "e122" (func 0)) (export "e123" (func 0)) (export "e124" (func 0)) (export "e125" (func 0)) (export "e126" (func 0)) (export "e127" (func 0)) (export "e128" (func 0)) (export "e129" (func 0)) (export "e130" (func 0)) (export "e131" (func 0)) (export "e132" (func 0)) (export "e133" (func 0)) (export "e134" (func 0)) (export "e135" (func 0)) (export "e136" (func 0)) (export "e137" (func 0)) (export "e138" (func 0)) (export "e139" (func 0)) (export "e140" (func 0)) (export "e141" (func 0)) (export "e142" (func 0)) (export "e143" (func 0)) (export "e144" (func 0)) (export "e145" (func 0)) (export "e146" (func 0)) (export "e147" (func 0)) (export "e148" (func 0)) (export "e149" (func 0)) (export "e150" (func 0)) (export "e151" (func 0)) (export "e152" (func 0)) (export "e153" (func 0)) (export "e154" (func 0)) (export "e155" (func 0)) (export "e156" (func 0)) (export "e157" (func 0)) (export "e158" (func 0)) (export "e159" (func 0)) (export "e160" (func 0)) (export "e161" (func 0)) (export "e162" (func 0)) (export "e163" (func 0)) (export "e164" (func 0)) (export "e165" (func 0)) (export "e166" (func 0)) (export "e167" (func 0)) (export "e168" (func 0)) (export "e169" (func 0)) (export "e170" (func 0)) (export "e171" (func 0)) (export "e172" (func 0)) (export "e173" (func 0)) (export "e174" (func 0)) (export "e175" (func 0)) (export "e176" (func 0)) (export "e177" (func 0)) (export "e178" (func 0)) (export "e179" (func 0)) (export "e180" (func 0)) (export "e181" (func 0)) (export "e182" (func 0)) (export "e183" (func 0)) (export "e184" (func 0)) (export "e185" (func 0)) (export "e186" (func 0)) (export "e187" (func 0)) (export "e188" (func 0)) (export "e189" (func 0)) (export "e190" (func 0)) (export "e191" (func 0)) (export "e192" (func 0)) (export "e193" (func 0)) (export "e194" (func 0)) (export "e195" (func 0)) (export "e196" (func 0)) (export "e197" (func 0)) (export "e198" (func 0)) (export "e199" (func 0)) (export "e200" (func 0)) (export "e201" (func 0)) (export "e202" (func 0)) (export "e203" (func 0)) (export "e204" (func 0)) (export "e205" (func 0)) (export "e206" (func 0)) (export "e207" (func 0)) (export "e208" (func 0)) (export "e209" (func 0)) (export "e210" (func 0)) (export "e211" (func 0)) (export "e212" (func 0)) (export "e213" (func 0)) (export "e214" (func 0)) (export "e215" (func 0)) (export "e216" (func 0)) (export "e217" (func 0)) (export "e218" (func 0)) (export "e219" (func 0)) (export "e220" (func 0)) (export "e221" (func 0)) (export "e222" (func 0)) (export "e223" (func 0)) (export "e224" (func 0)) (export "e225" (func 0)) (export "e226" (func 0)) (export "e227" (func 0)) (export "e228" (func 0)) (export "e229" (func 0)) (export "e230" (func 0)) (export "e231" (func 0)) (export "e232" (func 0)) (export "e233" (func 0)) (export "e234" (func 0)) (export "e235" (func 0)) (export "e236" (func 0)) (export "e237" (func 0)) (export "e238" (func 0)) (export "e239" (func 0)) (export "e240" (func 0)) (export "e241" (func 0)) (export "e242" (func 0)) (export "e243" (func 0)) (export "e244" (func 0)) (export "e245" (func 0)) (export "e246" (func 0)) (export "e247" (func 0)) (export "e248" (func 0)) (export "e249" (func 0)) (export "e250" (func 0)) (export "e251" (func 0)) (export "e252" (func 0)) (export "e253" (func 0)) (export "e254" (func 0)) (export "e255" (func 0)) (export "e256" (func 0)) (export "e257" (func 0)) (export "e258" (func 0)) (export "e259" (func 0)) (export "e260" (func 0)) (export "e261" (func 0)) (export "e262" (func 0)) (export "e263" (func 0)) (export "e264" (func 0)) (export "e265" (func 0)) (export "e266" (func 0)) (export "e267" (func 0)) (export "e268" (func 0)) (export "e269" (func 0)) (export "e270" (func 0)) (export "e271" (func 0)) (export "e272" (func 0)) (export "e273" (func 0)) (export "e274" (func 0)) (export "e275" (func 0)) (export "e276" (func 0)) (export "e277" (func 0)) (export "e278" (func 0)) (export "e279" (func 0)) (export "e280" (func 0)) (export "e281" (func 0)) (export "e282" (func 0)) (export "e283" (func 0)) (export "e284" (func 0)) (export "e285" (func 0)) (export "e286" (func 0)) (export "e287" (func 0)) (export "e288" (func 0)) (export "e289" (func 0)) (export "e290" (func 0)) (export "e291" (func 0)) (export "e292" (func 0)) (export "e293" (func 0)) (export "e294" (func 0)) (export "e295" (func 0)) (export "e296" (func 0)) (export "e297" (func 0)) (export "e298" (func 0)) (export "e299" (func 0)) (export "e300" (func 0)) (export "e301" (func 0)) (export "e302" (func 0)) (export "e303" (func 0)) (export "e304" (func 0)) (export "e305" (func 0)) (export "e306" (func 0)) (export "e307" (func 0)) (export "e308" (func 0)) (export "e309" (func 0)) (export "e310" (func 0)) (export "e311" (func 0)) (export "e312" (func 0)) (export "e313" (func 0)) (export "e314" (func 0)) (export "e315" (func 0)) (export "e316" (func 0)) (export "e317" (func 0)) (export "e318" (func 0)) (export "e319" (func 0)) (export "e320" (func 0)) (export "e321" (func 0)) (export "e322" (func 0)) (export "e323" (func 0)) (export "e324" (func 0)) (export "e325" (func 0)) (export "e326" (func 0)) (export "e327" (func 0)) (export "e328" (func 0)) (export "e329" (func 0)) (export "e330" (func 0)) (export "e331" (func 0)) (export "e332" (func 0)) (export "e333" (func 0)) (export "e334" (func 0)) (export "e335" (func 0)) (export "e336" (func 0)) (export "e337" (func 0)) (export "e338" (func 0)) (export "e339" (func 0)) (export "e340" (func 0)) (export "e341" (func 0)) (export "e342" (func 0)) (export "e343" (func 0)) (export "e344" (func 0)) (export "e345" (func 0)) (export "e346" (func 0)) (export "e347" (func 0)) (export "e348" (func 0)) (export "e349" (func 0)) (export "e350" (func 0)) (export "e351" (func 0)) (export "e352" (func 0)) (export "e353" (func 0)) (export "e354" (func 0)) (export "e355" (func 0)) (export "e356" (func 0)) (export "e357" (func 0)) (export "e358" (func 0)) (export "e359" (func 0)) (export "e360" (func 0)) (export "e361" (func 0)) (export "e362" (func 0)) (export "e363" (func 0)) (export "e364" (func 0)) (export "e365" (func 0)) (export "e366" (func 0)) (export "e367" (func 0)) (export "e368" (func 0)) (export "e369" (func 0)) (export "e370" (func 0)) (export "e371" (func 0)) (export "e372" (func 0)) (export "e373" (func 0)) (export "e374" (func 0)) (export "e375" (func 0)) (export "e376" (func 0)) (export "e377" (func 0)) (export "e378" (func 0)) (export "e379" (func 0)) (export "e380" (func 0)) (export "e381" (func 0)) (export "e382" (func 0)) (export "e383" (func 0)) (export "e384" (func 0)) (export "e385" (func 0)) (export "e386" (func 0)) (export "e387" (func 0)) (export "e388" (func 0)) (export "e389" (func 0)) (export "e390" (func 0)) (export "e391" (func 0)) (export "e392" (func 0)) (export "e393" (func 0)) (export "e394" (func 0)) (export "e395" (func 0)) (export "e396" (func 0)) (export "e397" (func 0)) (export "e398" (func 0)) (export "e399" (func 0)) (export "e400" (func 0)) (export "e401" (func 0)) (export "e402" (func 0)) (export "e403" (func 0)) (export "e404" (func 0)) (export "e405" (func 0)) (export "e406" (func 0)) (export "e407" (func 0)) (export "e408" (func 0)) (export "e409" (func 0)) (export "e410" (func 0)) (export "e411" (func 0)) (export "e412" (func 0)) (export "e413" (func 0)) (export "e414" (func 0)) (export "e415" (func 0)) (export "e416" (func 0)) (export "e417" (func 0)) (export "e418" (func 0)) (export "e419" (func 0)) (export "e420" (func 0)) (export "e421" (func 0)) (export "e422" (func 0)) (export "e423" (func 0)) (export "e424" (func 0)) (export "e425" (func 0)) (export "e426" (func 0)) (export "e427" (func 0)) (export "e428" (func 0)) (export "e429" (func 0)) (export "e430" (func 0)) (export "e431" (func 0)) (export "e432" (func 0)) (export "e433" (func 0)) (export "e434" (func 0)) (export "e435" (func 0)) (export "e436" (func 0)) (export "e437" (func 0)) (export "e438" (func 0)) (export "e439" (func 0)) (export "e440" (func 0)) (export "e441" (func 0)) (export "e442" (func 0)) (export "e443" (func 0)) (export "e444" (func 0)) (export "e445" (func 0)) (export "e446" (func 0)) (export "e447" (func 0)) (export "e448" (func 0)) (export "e449" (func 0)) (export "e450" (func 0)) (export "e451" (func 0)) (export "e452" (func 0)) (export "e453" (func 0)) (export "e454" (func 0)) (export "e455" (func 0)) (export "e456" (func 0)) (export "e457" (func 0)) (export "e458" (func 0)) (export "e459" (func 0)) (export "e460" (func 0)) (export "e461" (func 0)) (export "e462" (func 0)) (export "e463" (func 0)) (export "e464" (func 0)) (export "e465" (func 0)) (export "e466" (func 0)) (export "e467" (func 0)) (export "e468" (func 0)) (export "e469" (func 0)) (export "e470" (func 0)) (export "e471" (func 0)) (export "e472" (func 0)) (export "e473" (func 0)) (export "e474" (func 0)) (export "e475" (func 0)) (export "e476" (func 0)) (export "e477" (func 0)) (export "e478" (func 0)) (export "e479" (func 0)) (export "e480" (func 0)) (export "e481" (func 0)) (export "e482" (func 0)) (export "e483" (func 0)) (export "e484" (func 0)) (export "e485" (func 0)) (export "e486" (func 0)) (export "e487" (func 0)) (export "e488" (func 0)) (export "e489" (func 0)) (export "e490" (func 0)) (export "e491" (func 0)) (export "e492" (func 0)) (export "e493" (func 0)) (export "e494" (func 0)) (export "e495" (func 0)) (export "e496" (func 0)) (export "e497" (func 0)) (export "e498" (func 0)) (export "e499" (func 0)) (export "e500" (func 0)) (export "e501" (func 0)) (export "e502" (func 0)) (export "e503" (func 0)) (export "e504" (func 0)) (export "e505" (func 0)) (export "e506" (func 0)) (export "e507" (func 0)) (export "e508" (func 0)) (export "e509" (func 0)) (export "e510" (func 0)) (export "e511" (func 0)) (export "e512" (func 0)) (export "e513" (func 0)) (export "e514" (func 0)) (export "e515" (func 0)) (export "e516" (func 0)) (export "e517" (func 0)) (export "e518" (func 0)) (export "e519" (func 0)) (export "e520" (func 0)) (export "e521" (func 0)) (export "e522" (func 0)) (export "e523" (func 0)) (export "e524" (func 0)) (export "e525" (func 0)) (export "e526" (func 0)) (export "e527" (func 0)) (export "e528" (func 0)) (export "e529" (func 0)) (export "e530" (func 0)) (export "e531" (func 0)) (export "e532" (func 0)) (export "e533" (func 0)) (export "e534" (func 0)) (export "e535" (func 0)) (export "e536" (func 0)) (export "e537" (func 0)) (export "e538" (func 0)) (export "e539" (func 0)) (export "e540" (func 0)) (export "e541" (func 0)) (export "e542" (func 0)) (export "e543" (func 0)) (export "e544" (func 0)) (export "e545" (func 0)) (export "e546" (func 0)) (export "e547" (func 0)) (export "e548" (func 0)) (export "e549" (func 0)) (export "e550" (func 0)) (export "e551" (func 0)) (export "e552" (func 0)) (export "e553" (func 0)) (export "e554" (func 0)) (export "e555" (func 0)) (export "e556" (func 0)) (export "e557" (func 0)) (export "e558" (func 0)) (export "e559" (func 0)) (export "e560" (func 0)) (export "e561" (func 0)) (export "e562" (func 0)) (export "e563" (func 0)) (export "e564" (func 0)) (export "e565" (func 0)) (export "e566" (func 0)) (export "e567" (func 0)) (export "e568" (func 0)) (export "e569" (func 0)) (export "e570" (func 0)) (export "e571" (func 0)) (export "e572" (func 0)) (export "e573" (func 0)) (export "e574" (func 0)) (export "e575" (func 0)) (export "e576" (func 0)) (export "e577" (func 0)) (export "e578" (func 0)) (export "e579" (func 0)) (export "e580" (func 0)) (export "e581" (func 0)) (export "e582" (func 0)) (export "e583" (func 0)) (export "e584" (func 0)) (export "e585" (func 0)) (export "e586" (func 0)) (export "e587" (func 0)) (export "e588" (func 0)) (export "e589" (func 0)) (export "e590" (func 0)) (export "e591" (func 0)) (export "e592" (func 0)) (export "e593" (func 0)) (export "e594" (func 0)) (export "e595" (func 0)) (export "e596" (func 0)) (export "e597" (func 0)) (export "e598" (func 0)) (export "e599" (func 0)) (export "e600" (func 0)) (export "e601" (func 0)) (export "e602" (func 0)) (export "e603" (func 0)) (export "e604" (func 0)) (export "e605" (func 0)) (export "e606" (func 0)) (export "e607" (func 0)) (export "e608" (func 0)) (export "e609" (func 0)) (export "e610" (func 0)) (export "e611" (func 0)) (export "e612" (func 0)) (export "e613" (func 0)) (export "e614" (func 0)) (export "e615" (func 0)) (export "e616" (func 0)) (export "e617" (func 0)) (export "e618" (func 0)) (export "e619" (func 0)) (export "e620" (func 0)) (export "e621" (func 0)) (export "e622" (func 0)) (export "e623" (func 0)) (export "e624" (func 0)) (export "e625" (func 0)) (export "e626" (func 0)) (export "e627" (func 0)) (export "e628" (func 0)) (export "e629" (func 0)) (export "e630" (func 0)) (export "e631" (func 0)) (export "e632" (func 0)) (export "e633" (func 0)) (export "e634" (func 0)) (export "e635" (func 0)) (export "e636" (func 0)) (export "e637" (func 0)) (export "e638" (func 0)) (export "e639" (func 0)) (export "e640" (func 0)) (export "e641" (func 0)) (export "e642" (func 0)) (export "e643" (func 0)) (export "e644" (func 0)) (export "e645" (func 0)) (export "e646" (func 0)) (export "e647" (func 0)) (export "e648" (func 0)) (export "e649" (func 0)) (export "e650" (func 0)) (export "e651" (func 0)) (export "e652" (func 0)) (export "e653" (func 0)) (export "e654" (func 0)) (export "e655" (func 0)) (export "e656" (func 0)) (export "e657" (func 0)) (export "e658" (func 0)) (export "e659" (func 0)) (export "e660" (func 0)) (export "e661" (func 0)) (export "e662" (func 0)) (export "e663" (func 0)) (export "e664" (func 0)) (export "e665" (func 0)) (export "e666" (func 0)) (export "e667" (func 0)) (export "e668" (func 0)) (export "e669" (func 0)) (export "e670" (func 0)) (export "e671" (func 0)) (export "e672" (func 0)) (export "e673" (func 0)) (export "e674" (func 0)) (export "e675" (func 0)) (export "e676" (func 0)) (export "e677" (func 0)) (export "e678" (func 0)) (export "e679" (func 0)) (export "e680" (func 0)) (export "e681" (func 0)) (export "e682" (func 0)) (export "e683" (func 0)) (export "e684" (func 0)) (export "e685" (func 0)) (export "e686" (func 0)) (export "e687" (func 0)) (export "e688" (func 0)) (export "e689" (func 0)) (export "e690" (func 0)) (export "e691" (func 0)) (export "e692" (func 0)) (export "e693" (func 0)) (export "e694" (func 0)) (export "e695" (func 0)) (export "e696" (func 0)) (export "e697" (func 0)) (export "e698" (func 0)) (export "e699" (func 0)) (export "e700" (func 0)) (export "e701" (func 0)) (export "e702" (func 0)) (export "e703" (func 0)) (export "e704" (func 0)) (export "e705" (func 0)) (export "e706" (func 0)) (export "e707" (func 0)) (export "e708" (func 0)) (export "e709" (func 0)) (export "e710" (func 0)) (export "e711" (func 0)) (export "e712" (func 0)) (export "e713" (func 0)) (export "e714" (func 0)) (export "e715" (func 0)) (export "e716" (func 0)) (export "e717" (func 0)) (export "e718" (func 0)) (export "e719" (func 0)) (export "e720" (func 0)) (export "e721" (func 0)) (export "e722" (func 0)) (export "e723" (func 0)) (export "e724" (func 0)) (export "e725" (func 0)) (export "e726" (func 0)) (export "e727" (func 0)) (export "e728" (func 0)) (export "e729" (func 0)) (export "e730" (func 0)) (export "e731" (func 0)) (export "e732" (func 0)) (export "e733" (func 0)) (export "e734" (func 0)) (export "e735" (func 0)) (export "e736" (func 0)) (export "e737" (func 0)) (export "e738" (func 0)) (export "e739" (func 0)) (export "e740" (func 0)) (export "e741" (func 0)) (export "e742" (func 0)) (export "e743" (func 0)) (export "e744" (func 0)) (export "e745" (func 0)) (export "e746" (func 0)) (export "e747" (func 0)) (export "e748" (func 0)) (export "e749" (func 0)) (export "e750" (func 0)) (export "e751" (func 0)) (export "e752" (func 0)) (export "e753" (func 0)) (export "e754" (func 0)) (export "e755" (func 0)) (export "e756" (func 0)) (export "e757" (func 0)) (export "e758" (func 0)) (export "e759" (func 0)) (export "e760" (func 0)) (export "e761" (func 0)) (export "e762" (func 0)) (export "e763" (func 0)) (export "e764" (func 0)) (export "e765" (func 0)) (export "e766" (func 0)) (export "e767" (func 0)) (export "e768" (func 0)) (export "e769" (func 0)) (export "e770" (func 0)) (export "e771" (func 0)) (export "e772" (func 0)) (export "e773" (func 0)) (export "e774" (func 0)) (export "e775" (func 0)) (export "e776" (func 0)) (export "e777" (func 0)) (export "e778" (func 0)) (export "e779" (func 0)) (export "e780" (func 0)) (export "e781" (func 0)) (export "e782" (func 0)) (export "e783" (func 0)) (export "e784" (func 0)) (export "e785" (func 0)) (export "e786" (func 0)) (export "e787" (func 0)) (export "e788" (func 0)) (export "e789" (func 0)) (export "e790" (func 0)) (export "e791" (func 0)) (export "e792" (func 0)) (export "e793" (func 0)) (export "e794" (func 0)) (export "e795" (func 0)) (export "e796" (func 0)) (export "e797" (func 0)) (export "e798" (func 0)) (export "e799" (func 0)) (export "e800" (func 0)) (export "e801" (func 0)) (export "e802" (func 0)) (export "e803" (func 0)) (export "e804" (func 0)) (export "e805" (func 0)) (export "e806" (func 0)) (export "e807" (func 0)) (export "e808" (func 0)) (export "e809" (func 0)) (export "e810" (func 0)) (export "e811" (func 0)) (export "e812" (func 0)) (export "e813" (func 0)) (export "e814" (func 0)) (export "e815" (func 0)) (export "e816" (func 0)) (export "e817" (func 0)) (export "e818" (func 0)) (export "e819" (func 0)) (export "e820" (func 0)) (export "e821" (func 0)) (export "e822" (func 0)) (export "e823" (func 0)) (export "e824" (func 0)) (export "e825" (func 0)) (export "e826" (func 0)) (export "e827" (func 0)) (export "e828" (func 0)) (export "e829" (func 0)) (export "e830" (func 0)) (export "e831" (func 0)) (export "e832" (func 0)) (export "e833" (func 0)) (export "e834" (func 0)) (export "e835" (func 0)) (export "e836" (func 0)) (export "e837" (func 0)) (export "e838" (func 0)) (export "e839" (func 0)) (export "e840" (func 0)) (export "e841" (func 0)) (export "e842" (func 0)) (export "e843" (func 0)) (export "e844" (func 0)) (export "e845" (func 0)) (export "e846" (func 0)) (export "e847" (func 0)) (export "e848" (func 0)) (export "e849" (func 0)) (export "e850" (func 0)) (export "e851" (func 0)) (export "e852" (func 0)) (export "e853" (func 0)) (export "e854" (func 0)) (export "e855" (func 0)) (export "e856" (func 0)) (export "e857" (func 0)) (export "e858" (func 0)) (export "e859" (func 0)) (export "e860" (func 0)) (export "e861" (func 0)) (export "e862" (func 0)) (export "e863" (func 0)) (export "e864" (func 0)) (export "e865" (func 0)) (export "e866" (func 0)) (export "e867" (func 0)) (export "e868" (func 0)) (export "e869" (func 0)) (export "e870" (func 0)) (export "e871" (func 0)) (export "e872" (func 0)) (export "e873" (func 0)) (export "e874" (func 0)) (export "e875" (func 0)) (export "e876" (func 0)) (export "e877" (func 0)) (export "e878" (func 0)) (export "e879" (func 0)) (export "e880" (func 0)) (export "e881" (func 0)) (export "e882" (func 0)) (export "e883" (func 0)) (export "e884" (func 0)) (export "e885" (func 0)) (export "e886" (func 0)) (export "e887" (func 0)) (export "e888" (func 0)) (export "e889" (func 0)) (export "e890" (func 0)) (export "e891" (func 0)) (export "e892" (func 0)) (export "e893" (func 0)) (export "e894" (func 0)) (export "e895" (func 0)) (export "e896" (func 0)) (export "e897" (func 0)) (export "e898" (func 0)) (export "e899" (func 0)) (export "e900" (func 0)) (export "e901" (func 0)) (export "e902" (func 0)) (export "e903" (func 0)) (export "e904" (func 0)) (export "e905" (func 0)) (export "e906" (func 0)) (export "e907" (func 0)) (export "e908" (func 0)) (export "e909" (func 0)) (export "e910" (func 0)) (export "e911" (func 0)) (export "e912" (func 0)) (export "e913" (func 0)) (export "e914" (func 0)) (export "e915" (func 0)) (export "e916" (func 0)) (export "e917" (func 0)) (export "e918" (func 0)) (export "e919" (func 0)) (export "e920" (func 0)) (export "e921" (func 0)) (export "e922" (func 0)) (export "e923" (func 0)) (export "e924" (func 0)) (export "e925" (func 0)) (export "e926" (func 0)) (export "e927" (func 0)) (export "e928" (func 0)) (export "e929" (func 0)) (export "e930" (func 0)) (export "e931" (func 0)) (export "e932" (func 0)) (export "e933" (func 0)) (export "e934" (func 0)) (export "e935" (func 0)) (export "e936" (func 0)) (export "e937" (func 0)) (export "e938" (func 0)) (export "e939" (func 0)) (export "e940" (func 0)) (export "e941" (func 0)) (export "e942" (func 0)) (export "e943" (func 0)) (export "e944" (func 0)) (export "e945" (func 0)) (export "e946" (func 0)) (export "e947" (func 0)) (export "e948" (func 0)) (export "e949" (func 0)) (export "e950" (func 0)) (export "e951" (func 0)) (export "e952" (func 0)) (export "e953" (func 0)) (export "e954" (func 0)) (export "e955" (func 0)) (export "e956" (func 0)) (export "e957" (func 0)) (export "e958" (func 0)) (export "e959" (func 0)) (export "e960" (func 0)) (export "e961" (func 0)) (export "e962" (func 0)) (export "e963" (func 0)) (export "e964" (func 0)) (export "e965" (func 0)) (export "e966" (func 0)) (export "e967" (func 0)) (export "e968" (func 0)) (export "e969" (func 0)) (export "e970" (func 0)) (export "e971" (func 0)) (export "e972" (func 0)) (export "e973" (func 0)) (export "e974" (func 0)) (export "e975" (func 0)) (export "e976" (func 0)) (export "e977" (func 0)) (export "e978" (func 0)) (export "e979" (func 0)) (export "e980" (func 0)) (export "e981" (func 0)) (export "e982" (func 0)) (export "e983" (func 0)) (export "e984" (func 0)) (export "e985" (func 0)) (export "e986" (func 0)) (export "e987" (func 0)) (export "e988" (func 0)) (export "e989" (func 0)) (export "e990" (func 0)) (export "e991" (func 0)) (export "e992" (func 0)) (export "e993" (func 0)) (export "e994" (func 0)) (export "e995" (func 0)) (export "e996" (func 0)) (export "e997" (func 0)) (export "e998" (func 0)) (export "e999" (func 0)) (export "e1000" (func 0)) (export "e1001" (func 0)) (export "e1002" (func 0)) (export "e1003" (func 0)) (export "e1004" (func 0)) (export "e1005" (func 0)) (export "e1006" (func 0)) (export "e1007" (func 0)) (export "e1008" (func 0)) (export "e1009" (func 0)) (export "e1010" (func 0)) (export "e1011" (func 0)) (export "e1012" (func 0)) (export "e1013" (func 0)) (export "e1014" (func 0)) (export "e1015" (func 0)) (export "e1016" (func 0)) (export "e1017" (func 0)) (export "e1018" (func 0)) (export "e1019" (func 0)) (export "e1020" (func 0)) (export "e1021" (func 0)) (export "e1022" (func 0)) (export "e1023" (func 0)) (export "e1024" (func 0)) ) diff --git a/wai/quantum-conformance/wasmq/refuse/trailing-byte.wai b/wai/quantum-conformance/wasmq/refuse/trailing-byte.wai new file mode 100644 index 000000000..537b6c9cd Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/trailing-byte.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/truncated.wai b/wai/quantum-conformance/wasmq/refuse/truncated.wai new file mode 100644 index 000000000..6bbd72135 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/truncated.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/two-callouts-for-one-extern.wai b/wai/quantum-conformance/wasmq/refuse/two-callouts-for-one-extern.wai new file mode 100644 index 000000000..017377cd0 Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/two-callouts-for-one-extern.wai differ diff --git a/wai/quantum-conformance/wasmq/refuse/work-past-what-a-run-may-ask.wai b/wai/quantum-conformance/wasmq/refuse/work-past-what-a-run-may-ask.wai new file mode 100644 index 000000000..6506cb55d Binary files /dev/null and b/wai/quantum-conformance/wasmq/refuse/work-past-what-a-run-may-ask.wai differ diff --git a/wai/quantum-conformance/wasmq/vectors.json b/wai/quantum-conformance/wasmq/vectors.json new file mode 100644 index 000000000..6980a03a2 --- /dev/null +++ b/wai/quantum-conformance/wasmq/vectors.json @@ -0,0 +1,845 @@ +{ + "profile": "wai.quantum.wasmq/1", + "note": "every run vector gives exactly `expect` on every engine and every host; every refusal is refused with its code (extensions/quantum-wasmq.md §9)", + "runs": [ + { + "file": "envelopes/surface3-decode64.wai", + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 836380, + "callouts": 32, + "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", + "result_blake3": "35a686ec1b71982c65d7519e24166353ad35e8114ca03cf0003d38a93f7b06eb", + "receipt_digest": "df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1", + "job_blake3": "80676447542d8436b8c26afa4ef13aedb526f13c02c117abf50c970cb589b241" + } + }, + { + "file": "envelopes/surface5-decode64-job.wai", + "envelope_blake3": "3691fd8cac6fffe58d46a496aeff248a40d654129fe8cd23bae3dd6d3cebf8f4", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 765610, + "callouts": 16, + "transcript_blake3": "eb5252076ea8bb4c45ccec714a3cc64b89e833fe0a49c36a28881b63b1c3799e", + "result_blake3": "c38c0aa8b9f6432b7fe4776d8d58b270893f683901c53a73742a407004f92b66", + "receipt_digest": "5109fd0c11be13f637530e8861ec2d625a2985a9a3ad9340e0d00d36aea2346d", + "job_blake3": "dcf23b94abc66a4b0ab701c80fa8d0215bbf24e044261211153d330dcf921817" + } + }, + { + "file": "envelopes/gross-decode.wai", + "envelope_blake3": "0e62b798fc22ee783fcdf318188ab697f50c567487f71b512a5c555a7dd46cda", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 1021912, + "callouts": 4, + "transcript_blake3": "29b7d81e2ebc5d2dac6413ab3c15a2da2b2dcac03f3c68633a10bde60e59d3c4", + "result_blake3": "c193273cf7f0065b75c9a8baaf3c7e91f0f75b427b511a065c1d6ccd0bbded50", + "receipt_digest": "7d65a55b4c9839e807f4ee8a8dbc5ff79776967b3c9f828aa0efec8fa36fdb86", + "job_blake3": "085e75df38df048ba9198560191639d2f101329d0be38abebfed28a76009f09c" + } + }, + { + "file": "envelopes/atoms4x4-plan64.wai", + "envelope_blake3": "ef6d0807dc70169b792feec7d08bfe824ec104cebd56ae4df18e5520107ed479", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 576343, + "callouts": 32, + "transcript_blake3": "927caf8a41a7e9a654471532af1f428ddc86cb5efab5b0d64ba21fae2726d752", + "result_blake3": "5d688283421aac7bdb96350844120919d636aa481b6b662392ee132b471c0fe9", + "receipt_digest": "51e4e50d256f357866845d7678ed836a39b57e0faf141dd458cd2c69d91f4c47", + "job_blake3": "10e89d19dc3b037fc32cb04fdfd928b7b36bb58784182e404223ec8d16516105" + } + }, + { + "file": "envelopes/atoms9x9-plan.wai", + "envelope_blake3": "43647ef1419dcb7c0e584daa4201f8745f617141d14df8d668f75b35bb39cd44", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 914777, + "callouts": 6, + "transcript_blake3": "e280bbc8ba046e7f003d173c4c280050d63e1b972ec216c465ce67bed621ed6a", + "result_blake3": "bbcdd6333d35a7a59de45c970f39d11a7c08fc32df3abe4fdd211008b4d0e51f", + "receipt_digest": "e22f0fca1ff06a0135cf12607bda14a374bf45b393cd563e0795e82c1a41c554", + "job_blake3": "452b81e0d98b2fd78f4fd4852033d1c5b6112f6994001de5e09c178a36dba636" + } + }, + { + "file": "envelopes/list-echo.wai", + "envelope_blake3": "7ae91afc65551149a86ab445d24a28acaf52323f5aaa858d1859b0434ae156bc", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 224, + "callouts": 8, + "transcript_blake3": "3c53158450b92c3e6f15f7b735d7ef722b6c602d80c8dfd323f3d4f59408f710", + "result_blake3": "b934991a387b3cdb81dafe6c14bdbef57999f3954990b4089253796b605debe1", + "receipt_digest": "541548abbfe6224fe2523fb2f6133bb61e808e78f2757e635fdda8a395a4c425", + "job_blake3": "07eeda7a1acef2897e94c81389e009ef24d4c43c04153ea786ba30d1fc1c40c9" + } + }, + { + "file": "envelopes/reset-draws.wai", + "envelope_blake3": "8404b85c8109a980a8a8c21a56be5664f193fca55869a53e51fee4d2e466ea70", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 128, + "callouts": 64, + "transcript_blake3": "6030f402b046fc2ccce71fb0342fc46dd6da2347fabbceda3677815fa82472ed", + "result_blake3": "5780a171a07be0f783e2eacd8dc71a1a64d3b32c764f2d25adc148edd9c5ae50", + "receipt_digest": "5feb7d5ad3bdafb62a9c88bc4ea50a671a03a0d0e7375fbdabf5fb7260ac3f6b", + "job_blake3": "6dd8e6c35b99ca5ccff31681d313372f74f60dda2dbb88536c38b7ff31222d12" + } + }, + { + "file": "envelopes/nan-canonical.wai", + "envelope_blake3": "70571f247aeb32182ffa65b93404373656f13af4a53d6bd8338b471b2e0d7a31", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 64, + "callouts": 8, + "transcript_blake3": "d062b788107d413845ef253a9d9265548346393a28faeacfe3ef3be471ebfe7b", + "result_blake3": "2b79f2f7e4aa970c1e00dab10a32542337e551a8851c2d6f230b636cb22bd40a", + "receipt_digest": "5a3305371c79689531a7cbaab3187ae33bfdb522153537c16c8099c57ae0570a", + "job_blake3": "2e76d20a452820058c9bdb73069ca6238ab97a4e33c7c5a88096e99c4e574715" + } + }, + { + "file": "envelopes/grow-past-maximum.wai", + "envelope_blake3": "221f7185865476a51e4b5b1a483b2ab78392b40a5efc2c4a86dd064623c284cb", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 74896, + "callouts": 16, + "transcript_blake3": "0fee265d944e8a9d3b6076a0e1c50647847446a3ef01adcfc61b014768cb838a", + "result_blake3": "f0d60769b88b7b1916de9c31159015e01d9b628fbe4dde3558d2c022f0742007", + "receipt_digest": "c7b6269b9a67c3bc4b55e2f9a55856e52edcb8630a2daa89d5fbe430d33bd90c", + "job_blake3": "1c38d94473fcc2c9093db03355eb656e0db5311cc72fc31d11b690fa8625da00" + } + }, + { + "file": "envelopes/fault-fuel.wai", + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35", + "expect": { + "status": "fault", + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "fuel_used": 400, + "callouts": 1, + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "result_blake3": "0750cc44f1ec6c1f133f31cd2b63e5e5517b85351294eaf23d68d81ee0f02e86", + "receipt_digest": "9cab411ba32a647e5c4b0ce53609a4308c1a07bd26c817fca582e3e04258bff4", + "job_blake3": "28ea3f814025c3ac778a726cb4465ca6e24d041515aab9fcebf573766d3bbbb1" + } + }, + { + "file": "envelopes/fault-stack.wai", + "envelope_blake3": "760fcb114402553db1314cb63c50ff40ea3012aaa3620b0079e1b9684472cb5f", + "expect": { + "status": "fault", + "fault": { + "reason": "stack_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 71213, + "callout": "f" + }, + "fuel_used": 71213, + "callouts": 1, + "transcript_blake3": "312cb35563349cf18ce5206254a933ba0e98ac08d15519719347b39126997727", + "result_blake3": "d65de2d3cadb96fe4e258057ed230b2e4c9f9ec5c44ae6eea6b08720d3c7d386", + "receipt_digest": "6aff4dc5a0cb49865e2234ef66f484b3f5844d3fbffcef5ef3ad420e452d7af1", + "job_blake3": "e039a7f6a78a1e16c10cb84fb1dde963140f2ae83d43425ad54af44904dd4ea2" + } + }, + { + "file": "envelopes/fault-trap.wai", + "envelope_blake3": "8435227e277d8f3fdea2c7847be85aa618389ec9d7432d72d0170f940da7fa66", + "expect": { + "status": "fault", + "fault": { + "reason": "integer_divide_by_zero", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 6, + "callout": "f" + }, + "fuel_used": 6, + "callouts": 1, + "transcript_blake3": "7d4291653436ebc69debfc0881d22799cd6d1e972c2cc81d26d62aa3b8bdb12b", + "result_blake3": "87e38f767b2cf5d2648be7626b3e0c3a6fbd94a0020827b2894e6cd650406bd3", + "receipt_digest": "baaa13476d446f10eb9adcde00289198f713e0bdd02f886eb8ecfff56087caf4", + "job_blake3": "57ab50773e9e77dc909d3cd19d19e87ebde2020ba8abcabcc68eb3c27902843b" + } + }, + { + "file": "envelopes/fault-bad-result.wai", + "envelope_blake3": "91b61abdd43a815f26055fe5834b2cecc57ebe5951c8a41b55350f5ab6acba4d", + "expect": { + "status": "fault", + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f" + }, + "fuel_used": 4, + "callouts": 1, + "transcript_blake3": "f64338e17fa3fdc023f35081e59fb8c168742f65e7397d41527c037b587fad8c", + "result_blake3": "2a323d102043e328978b9628007e18b34f89dd4ed7e7e36f1b23d5578e3868f4", + "receipt_digest": "507ad19b8d4f6db08fa02b931ccf35dd1449dba10370ed17db495e83cb29616e", + "job_blake3": "4ebd23da949d94c36566dd46852bb399f6d2754ac399e4c8b232584f5b499a31" + } + }, + { + "file": "envelopes/fault-unreachable.wai", + "envelope_blake3": "8b952a5fb0fb3372da2348b1007a32b1114cbedae6e9f839bd2882d9744499ae", + "expect": { + "status": "fault", + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 1, + "callout": "f" + }, + "fuel_used": 1, + "callouts": 1, + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "result_blake3": "d56d9c1bd0b7002f18f229728b12202896e9eb6c99b743a6328fb74b9f99085e", + "receipt_digest": "687a172bf36f2ed86fc711216505aa3106547196fa88df296479ad13fb3357a1", + "job_blake3": "a71b55d916bfe51b36f48c5343a96ac142a841093f8d54fe2d4946b1ea8d8811" + } + }, + { + "file": "envelopes/fault-memory-out-of-bounds.wai", + "envelope_blake3": "a822060b2aa7c046e6c2663894fd92dacbb77cf30149895e810e6aebe078a3e3", + "expect": { + "status": "fault", + "fault": { + "reason": "memory_out_of_bounds", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 3, + "callout": "f" + }, + "fuel_used": 3, + "callouts": 1, + "transcript_blake3": "970f77653a32a719ccfc3b7778fb4f1ac30e46b6c3024734047a4910e191bee7", + "result_blake3": "2b54ec2727c92bd6eac308dc4b3898799fe510357c6dde77cca3551bf48fffe6", + "receipt_digest": "857570955a1dfc37fdbce9b57f98ab46ac6ce2b698d0dbce083bc3baec6972d9", + "job_blake3": "8f8df0039509bc30bf54bd6586654cdf019ae6f860f5e3fa53d758a818a42c2d" + } + }, + { + "file": "envelopes/fault-table-out-of-bounds.wai", + "envelope_blake3": "510a179c4ee42e8d17a98f4700944b276fd3cf344878764afd4ccd91416380dd", + "expect": { + "status": "fault", + "fault": { + "reason": "table_out_of_bounds", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f" + }, + "fuel_used": 4, + "callouts": 1, + "transcript_blake3": "dce5984f8ad188be19430448f3c84d0d74d282b4a688e8c6603e1e2ee3ff9416", + "result_blake3": "ce4abd2fe9f2ea73ef9aba52f6a4d8c24a9c37fb080c4847d4127d3d161e80b8", + "receipt_digest": "65d8fe86e24123231885301e3b64d045439aacdc16fd730ce5a5f649af60e7af", + "job_blake3": "b6c302ceb24eb0ef383a2d1fc8a98548710d88f88e62026edb99af4ce71591d3" + } + }, + { + "file": "envelopes/fault-indirect-call-null.wai", + "envelope_blake3": "552230daaa158f87386f51d2628aa1ceedf2f04ac287b6ecea7d891f98bfc3fe", + "expect": { + "status": "fault", + "fault": { + "reason": "indirect_call_null", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f" + }, + "fuel_used": 4, + "callouts": 1, + "transcript_blake3": "c7ef604fa47ea8019d723778fd27fa5c1042804271bda01b5af60135a82ee135", + "result_blake3": "f334e67b7ca2dea0fca173508c11f7d67eebfb2d0dab861b65f973a7dbc331fc", + "receipt_digest": "baceedce8975ac8e97ec3004b2e4e93ec56fde489d778a9a0184804687a2edec", + "job_blake3": "6c22b6e4842d9f56965ae68c981513f9fba996a1f823329754adfdc6e48e9b88" + } + }, + { + "file": "envelopes/fault-indirect-call-type.wai", + "envelope_blake3": "786e91c3d28aa7bcb0c85c4e434791fe75c383efa13aab39954e5364e5126cb7", + "expect": { + "status": "fault", + "fault": { + "reason": "indirect_call_type", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f" + }, + "fuel_used": 4, + "callouts": 1, + "transcript_blake3": "06822065549032408578f034a902cebfb74688d05201bc2b73f2d6f77649ae81", + "result_blake3": "5bff1a6a5784c951c6eb23da129ffb6b2c106113ece1c57ec124aecc9ab902df", + "receipt_digest": "6081e47cb523257269bb224f024b533083bbbb6fb6f993499f594d22886d5b2f", + "job_blake3": "7e80679f7af5d11c6deeef82c88a26bd05ec7501d543e3c9dab318d5845a6bf5" + } + }, + { + "file": "envelopes/fault-integer-overflow.wai", + "envelope_blake3": "61bb16b03eef03a4fca1df32058ede1ac4c70c616cff6a7da30ed5ee492619c3", + "expect": { + "status": "fault", + "fault": { + "reason": "integer_overflow", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f" + }, + "fuel_used": 4, + "callouts": 1, + "transcript_blake3": "8c4327d3f47ed561a49685961fb287930eef69aa0d1163a4554e6080b1994f00", + "result_blake3": "2029400253592096bb95d290cbbdbfc474c270844ae8cac41ddde12c8b967614", + "receipt_digest": "cf8576b72620376d1374d6d038db4012106125f0a20586fc17486f24340a9f1e", + "job_blake3": "ffbf1a33878511e2fb5b0a12b7e22ff02938913ae2348b7cadeabc29ad937d94" + } + }, + { + "file": "envelopes/fault-invalid-conversion.wai", + "envelope_blake3": "791ca6b1803e937865518c87b790ab2f69d393013e836dcac54ef84e521e9148", + "expect": { + "status": "fault", + "fault": { + "reason": "invalid_conversion_to_integer", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 3, + "callout": "f" + }, + "fuel_used": 3, + "callouts": 1, + "transcript_blake3": "682221395725ba345f0a06755334e1f369499d137039be09332d1200f189a78f", + "result_blake3": "73419905fce695c8e37d772c37d3a31b39a09e3c03e22c9beb2661d71dbd901b", + "receipt_digest": "4f8916210f1d09b02e2e36257f1e0265381db91abfe363e798b7c26f386e6d08", + "job_blake3": "43840b44a70525340f7a529800029f58c5421cb5f06f61000853a22076f1e664" + } + }, + { + "file": "envelopes/fault-in-seed.wai", + "envelope_blake3": "755210cc7eb29a5d8552a6bf2cf0907697fc94342ce1b179e1f3f29b15cbe027", + "expect": { + "status": "fault", + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "seed", + "module": "m", + "fuel": 1 + }, + "fuel_used": 1, + "callouts": 0, + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "result_blake3": "b4a18d79c3a734d523b51237ab88876d6bf817701ddbbd6ba11cd9da2729d20d", + "receipt_digest": "7a7c5ccfd180392daabef988f20ef14689263313d52f7283c0fba017c0f63421", + "job_blake3": "ef22734d1dd547edfd74146ec6f2be3bda4c8774f825667d9364227ad880fdce" + } + }, + { + "file": "envelopes/fault-in-init.wai", + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9", + "expect": { + "status": "fault", + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "init", + "module": "m", + "fuel": 1 + }, + "fuel_used": 1, + "callouts": 0, + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "result_blake3": "9e627dc2d4d1f23a87dbb51216b71c08a6be658d828cff58b73400f32146f8cd", + "receipt_digest": "ac633eb1fb15f62ad6df9b6b7891dc7ada2dac6c1503e5f4d073dc866b40cd6d", + "job_blake3": "fcaef3589572a03eeaf086b05bb9219a66e41b9bb418bd1e1bff4fa8ce128294" + } + }, + { + "file": "envelopes/fault-in-prior.wai", + "envelope_blake3": "6da2c86da1b3d288df324fa7902e4cb10c9c5b21160921907299402466c5d50c", + "expect": { + "status": "fault", + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "prior", + "module": "m", + "fuel": 8 + }, + "fuel_used": 8, + "callouts": 0, + "transcript_blake3": "3db8f511346d060c257940c086300666c8eb1b27c6083d36066b8d8f38e6408c", + "result_blake3": "219f8c11625c397b66e36e095ed5ec283c035d508c6e814fe72d838cb9b768d7", + "receipt_digest": "48f6ca3e7cc12a0ac8f277c342bf8098c9ca9d2938f855eb75336a33e0ce29b3", + "job_blake3": "0a2d5015081e5f5848684973109cbc1445c6245365aac604ad6f507c59e39ded" + } + }, + { + "file": "envelopes/fault-fuel-instance.wai", + "envelope_blake3": "36bafc2bdc98839a1e972ad5ffc1f193c64fdac6fdf45aa2c12cde86c887670c", + "expect": { + "status": "fault", + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "init", + "module": "m", + "fuel": 300, + "budget": "instance" + }, + "fuel_used": 300, + "callouts": 0, + "transcript_blake3": "be6859cdd3b1d9c43644d3b322c875f045610e4230b5a844ad8b44be02166c9e", + "result_blake3": "810fac02203ccff06024911ca5a9d516b218a723283097438b4030feddd8bd68", + "receipt_digest": "fb1b1f833cf07088f81e2589ee57fefade804bcf86f1464c44db605886e9dd13", + "job_blake3": "4bc66941f1fb52cc2e1775d72fd41aafe9b98075f4121e66777bb56c9325240a" + } + }, + { + "file": "envelopes/fault-fuel-run.wai", + "envelope_blake3": "0a19542a6a9178984a93d9863a1d5e9371e00b86c6e9143e04b5f0fb5b0be177", + "expect": { + "status": "fault", + "fault": { + "reason": "fuel_exhausted", + "shot": 2, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f", + "budget": "run" + }, + "fuel_used": 50, + "callouts": 3, + "transcript_blake3": "3772395e9bb2f9fba3b1a7ef4f5def3e5f10f76a9ba517d16056e5fd7cb7d413", + "result_blake3": "2d76e6afbe53c5a7c70efaf9e2718798801abb44db8f8e6ec6dcb41c5deb8f20", + "receipt_digest": "39102ed51a3b76f36d8fafb5a7f4d9af7c1d2363e74b5c1d6dfe593770b364ad", + "job_blake3": "d5d033c179132599093c62e203564454dbb7d1cedd4066990a4c5a207fd8a784" + } + }, + { + "file": "envelopes/list-post.wai", + "envelope_blake3": "00550280fb8c8cd69b62d9a72da2dd5e5a31a32c7563841d281d4559f6e33cd9", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 512, + "callouts": 8, + "transcript_blake3": "4391bccfcad1a60940f034c28422b29455fcb25bb9f01e08d29e2187dc2d1a0a", + "result_blake3": "69cdcbe7d145b7ff2a35fec1d9751101d56e8706fd1cd58ffbdf39fddee74811", + "receipt_digest": "046596e689946d057732ae72d01605a1b9dbd44f54b617c225a275fcaebb2ade", + "job_blake3": "1b8f89ceee027fab32ed7db06d364846fdfa0b74fe19b3e40a4e16710b801010" + } + }, + { + "file": "envelopes/fault-list-misaligned.wai", + "envelope_blake3": "86d7a11aae4324b6d693f9a4c328ba7d0fdd1356ca8f5745f7341d87a476117a", + "expect": { + "status": "fault", + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 25, + "callout": "f" + }, + "fuel_used": 25, + "callouts": 1, + "transcript_blake3": "83aeb6066d236324098b0d7505f7d7ac2b33d55efd46c2ab3aa25fe689f25a75", + "result_blake3": "3fca45ecabc10a75e9bd79e1a163f43507da91ee884ce9e97a9a2ea6473017e3", + "receipt_digest": "dfc140bf150a728b84df0ba88e703a4157728d6dfc4a4165623cd77bc9bbb99e", + "job_blake3": "8c508318fb58d5ee7c04197702751e2cc66a236aa5fa34c44284757f1da2c0cc" + } + }, + { + "file": "envelopes/fault-list-outside.wai", + "envelope_blake3": "50a51289910336b9abfbdcc3c20e4de2f5aeaeb45aedd5222122dad37e550bdc", + "expect": { + "status": "fault", + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 19, + "callout": "f" + }, + "fuel_used": 19, + "callouts": 1, + "transcript_blake3": "27065fd3a25dd0889a42b72268f297f9aa59762db626da94eb9f7c3f9000411c", + "result_blake3": "843876d7b0ea70197ad58158f52c8a926a8d7c6a18b4f8792da2119aab2424c7", + "receipt_digest": "5e4d3a2b7291919e7c49a8f9ef2fe1a75fb45de2b5d19800b057e21267f63a4f", + "job_blake3": "669dda2297272b8eea9108db7fcbe5138cfc32edcb634495140a2b8010d64b9d" + } + }, + { + "file": "envelopes/fault-list-length.wai", + "envelope_blake3": "e174412d171634231b2f67750a533c57b9ae3a770bc7cf05bda73ebbd7e35acc", + "expect": { + "status": "fault", + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 22, + "callout": "f" + }, + "fuel_used": 22, + "callouts": 1, + "transcript_blake3": "7551907ca0fc06cc89ad08083b41e47e182787d0c8e5a33cc3da590472b35cce", + "result_blake3": "93ff2af51423c90d07d71aebc1017725505a2616ad9c0b0828422bb6c2c4808a", + "receipt_digest": "9825d37bf34c4e19ef9d180b7af2b05fcf95bae26ff68f26df3c2dab8f1bf2c8", + "job_blake3": "8d49833a20416857978bf773fb2ca8250ad28b2dd2d59aaf354969a46c187185" + } + }, + { + "file": "envelopes/fault-list-past-its-bits.wai", + "envelope_blake3": "cf3530cd29b8e7acf002491efef49e048a67baf3359cbddd4df39b0a5253ca92", + "expect": { + "status": "fault", + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 31, + "callout": "f" + }, + "fuel_used": 31, + "callouts": 1, + "transcript_blake3": "80f92a68b68141246bc2d18e603bb3d9407022e0cc83b3c780e01040ee3d23f3", + "result_blake3": "b2ad46955b82125e262e0e91e67f8babecdb7ab297030ba34a57e65bce2d5aaf", + "receipt_digest": "7348d189ff128b563d5a0cca72d5d78630005b588dc0b34c0d9ebbaff519722e", + "job_blake3": "88e89c1dd2dfb1ac5df60b174c28293be28d6b4b17b5f091c3096214efb8a031" + } + }, + { + "file": "envelopes/signed-argument.wai", + "envelope_blake3": "e17c523aee4a14a137f1d42e9fd54f03d456e9a9ca347d8217c6da572297d46f", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 12, + "callouts": 2, + "transcript_blake3": "77633819db5e8f0a3e9c15758ee993940be0d03827d59c4fb2c9c1d2ba57171b", + "result_blake3": "df732d850cfb5f877e0adb6b340cacb97fefef59c211058b908db13619c3e3c3", + "receipt_digest": "508924a857c8e7cd5b0572ef745d4a7681488bce4f18dadcd6861f8a36e1c43b", + "job_blake3": "593401622b16525b8ba54c89e83537f644c594152424ad7c60ffeffdda656a7c" + } + }, + { + "file": "envelopes/measures-nothing.wai", + "envelope_blake3": "5e78032dc7e8f53a8d7f2682bf4ac710f823b85efe1b2871c7104fc1026ec4d6", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 32, + "callouts": 16, + "transcript_blake3": "125fff6fc9ef837133e8a32536d72cf17bb2eba5339cb44e0670de28e603c223", + "result_blake3": "459a3f37e623b8da48057e7e51b757e32eef2afa63e43feb1741cf3bb121a549", + "receipt_digest": "b42926844630b5a6fe803649f349ab027021d17c2ffdcedf77bb09fde80090ac", + "job_blake3": "5f5cc09266d9ed83d09ffe65d37e9684ff674af6339ca0919e8bbd169013aec9" + } + }, + { + "file": "envelopes/bits-nothing-writes.wai", + "envelope_blake3": "6021d8dea3fb04aa33d39bad7d8a3b8d9c9db917b39e7704247ef21a87f0207b", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 4, + "callouts": 2, + "transcript_blake3": "2d5add095f9856982ef6c371d9bcebc5b151c054e32243b1c216decb192df0fd", + "result_blake3": "8d03867d3a6d2942af8db247eb09ec91c612f5b60ca1f585269961e011262bc0", + "receipt_digest": "57999dae4d22e2d33dea1aa34c1522d49e751a96f8c41bd6914c1ed099f7dc76", + "job_blake3": "34b6358c0e0e00a060de35c1f8b995344ddcb162503e0bc04465986d8876bf3d" + } + }, + { + "file": "envelopes/block-bit-not-in-outcome.wai", + "envelope_blake3": "8d3bd92000e4f20f1457cc9d945fa6b1b492c47fe97024b69cd4902c65cef293", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 4, + "callouts": 2, + "transcript_blake3": "1c701f685b85c50710f9fc16977cf32be2383a745259abb8d18127dd5893213a", + "result_blake3": "c5b9cc7d75f29d9287b3e82c908ec649b3ea9a4daabb468b3b57bb10551e6158", + "receipt_digest": "cc99b5b572b3af6c609f8d1d13c1fa8ce57b7528cc8a7d2dc77dca6e3e0e622b", + "job_blake3": "17c7efa810d0205c72b07932da512b8445928a3933b71d99c7e7adfc5742f323" + } + }, + { + "file": "envelopes/two-modules-in-order.wai", + "envelope_blake3": "31ec7ef4b7522926bb950f0e5e148f9824c5ffb0f19cba103da1a6779c747383", + "expect": { + "status": "fault", + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "init", + "module": "first", + "fuel": 1 + }, + "fuel_used": 1, + "callouts": 0, + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "result_blake3": "a47f66a5d39469cae3d11ba433004cecda67ec2289ee0082d26bc6da44173771", + "receipt_digest": "4188a615da097c3d332a95590d64f546799f44357abb7cdf8dc64e6ded6de71a", + "job_blake3": "4fb6e4306cba4a897518f3745155c2bd564fe4387760696d67e24cebcdecf113" + } + }, + { + "file": "envelopes/physical-qubits.wai", + "envelope_blake3": "4c193ee3085e2812b52d3acd32187f3cad6955b4f437ae3abc9385a4dd192bc0", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 8, + "callouts": 4, + "transcript_blake3": "4aa09b443e84b2e03863cdbec2c470604aa1c9f6c4833e9bb014772e574e9674", + "result_blake3": "92863754bfaf6411b6bf9af47e17565a3bacec1e200822d50428ae8c401d9852", + "receipt_digest": "1c64f060044be74c24fe6091d86d0ed6ac51b9255d1455a7b4cc3fc19c054142", + "job_blake3": "cab385d980f1b05c04fd45cf4ba2c83fe7e3aeae52ef033dae841d7e6b6a04e0" + } + } + ], + "refuse": [ + { + "file": "refuse/body-without-end.wasm", + "code": "W001" + }, + { + "file": "refuse/function-of-no-type.wasm", + "code": "W001" + }, + { + "file": "refuse/import-clock.wasm", + "code": "W002" + }, + { + "file": "refuse/import-random.wasm", + "code": "W002" + }, + { + "file": "refuse/memory-unbounded.wasm", + "code": "W003" + }, + { + "file": "refuse/memory-too-large.wasm", + "code": "W003" + }, + { + "file": "refuse/start-function.wasm", + "code": "W004" + }, + { + "file": "refuse/shared-memory.wasm", + "code": "W005" + }, + { + "file": "refuse/atomic.wasm", + "code": "W005" + }, + { + "file": "refuse/simd.wasm", + "code": "W006" + }, + { + "file": "refuse/relaxed-simd.wasm", + "code": "W006" + }, + { + "file": "refuse/tail-call.wasm", + "code": "W007" + }, + { + "file": "refuse/ref-null.wasm", + "code": "W007" + }, + { + "file": "refuse/reference-type.wasm", + "code": "W007" + }, + { + "file": "refuse/table-grow.wasm", + "code": "W008" + }, + { + "file": "refuse/table-size.wasm", + "code": "W008" + }, + { + "file": "refuse/frame-too-large.wasm", + "code": "W009" + }, + { + "file": "refuse/too-many-exports.wasm", + "code": "W010" + }, + { + "file": "refuse/data-past-memory.wasm", + "code": "W011" + }, + { + "file": "refuse/reserved-export.wasm", + "code": "W012" + }, + { + "file": "refuse/seed-of-another-type.wasm", + "code": "W012" + }, + { + "file": "refuse/callouts-out-of-order.wai", + "code": "W015" + }, + { + "file": "refuse/two-callouts-for-one-extern.wai", + "code": "W015" + }, + { + "file": "refuse/callout-of-another-type.wai", + "code": "W015" + }, + { + "file": "refuse/memory-not-exported.wai", + "code": "W015" + }, + { + "file": "refuse/gate-outside-the-clifford-gates.wai", + "code": "W016" + }, + { + "file": "refuse/program-calls-nothing.wai", + "code": "W016" + }, + { + "file": "refuse/program-measure-with-no-bit.wai", + "code": "W016" + }, + { + "file": "refuse/no-shots.wai", + "code": "W017" + }, + { + "file": "refuse/instances-of-no-kind.wai", + "code": "W017" + }, + { + "file": "refuse/seed-a-double-cannot-hold.wai", + "code": "W017" + }, + { + "file": "refuse/memory-past-a-run-s-among-its-modules.wai", + "code": "W003" + }, + { + "file": "refuse/results-past-what-a-host-holds.wai", + "code": "W017" + }, + { + "file": "refuse/program-past-its-steps.wai", + "code": "W016" + }, + { + "file": "refuse/work-past-what-a-run-may-ask.wai", + "code": "W017" + }, + { + "file": "refuse/prior-hash-mismatch.wai", + "code": "W014" + }, + { + "file": "refuse/module-hash-mismatch.wai", + "code": "W014" + }, + { + "file": "refuse/program-hash-mismatch.wai", + "code": "W014" + }, + { + "file": "refuse/contract-run-changed.wai", + "code": "W013" + }, + { + "file": "refuse/contract-extra-member.wai", + "code": "W013" + }, + { + "file": "refuse/contract-not-canonical.wai", + "code": "W013" + }, + { + "file": "refuse/payload-of-another-kind.wai", + "code": "W013" + }, + { + "file": "refuse/byte-after-the-frames.wai", + "code": "W013" + }, + { + "file": "refuse/manifest-changed.wai", + "code": "W013" + }, + { + "file": "refuse/other-capability.wai", + "code": "W013" + }, + { + "file": "refuse/trailing-byte.wai", + "code": "W013" + }, + { + "file": "refuse/truncated.wai", + "code": "W013" + } + ] +} diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index ab6f10cf7..6a393325f 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -2118,6 +2118,20 @@ "edition": null, "registered": "2026-08-01" }, + { + "capability": "wai.quantum.wasmq", + "kind": "payload", + "status": "registered", + "media": "binary", + "role": "standalone", + "tier": "lossless", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-11-05" + }, { "capability": "wai.route.record", "kind": "record", @@ -3239,6 +3253,26 @@ "label": "wai.quantum.vml", "reason": "a module label: wai-quantum's `quantum_vml`, variational quantum machine learning" }, + { + "label": "wai.quantum.wasmq.result/1", + "reason": "a document with no schema file: the result of a wasmQ run, held to its form by wai-quantum's `engine::wasmq` (quantum-wasmq §6.4, §6.5)" + }, + { + "label": "wai.quantum.wasmq/1", + "reason": "a contract revision: the `profile` of a wai.quantum.wasmq payload's contract and of its run's result (quantum-wasmq §2.2, §6.4, §10), a part of the capability's definition" + }, + { + "label": "wai.quantum.wasmq/1x", + "reason": "refused on purpose: the wasmQ profile one letter longer; the receipts' differential check puts it in a contract to see both verifiers refuse the job (quantum-wasmq §6.5, rule 4)" + }, + { + "label": "wai.quantum.wasmq/2", + "reason": "refused on purpose: a wasmQ profile that no text defines; wasmQ's tests, the receipts corpus and its verifiers put it in a contract or a result to see the job refused (quantum-wasmq §6.5, rules 4 and 11)" + }, + { + "label": "wai.quantum.wasmx", + "reason": "refused on purpose: not a capability; wasmQ's adversarial test and its vectors put it in a payload's manifest to see the envelope refused with W013 (quantum-wasmq §2.3)" + }, { "label": "wai.quantum.window", "reason": "a module label: wai-quantum's `quantum_window`, decoding as the syndrome arrives" diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index 51e08b4db..99d6f89d4 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -91,6 +91,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.tree wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -193,6 +194,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.tree wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -314,6 +316,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.tree wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -416,6 +419,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.tree wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 2641ba667..fe5e0b07f 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -1548,6 +1548,18 @@ wai:wai.quantum.job a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-08-01"^^xsd:date . +wai:wai.quantum.wasmq a wai:Capability ; + rdfs:label "wai.quantum.wasmq" ; + wai:kind wai:kind-payload ; + wai:status wai:status-registered ; + wai:media wai:media-binary ; + wai:role wai:role-standalone ; + wai:tier wai:tier-lossless ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-11-05"^^xsd:date . + wai:wai.route.record a wai:Capability ; rdfs:label "wai.route.record" ; wai:kind wai:kind-record ; diff --git a/wai/wai-quantum-cli/Cargo.lock b/wai/wai-quantum-cli/Cargo.lock index 1164c3431..3e52df105 100644 --- a/wai/wai-quantum-cli/Cargo.lock +++ b/wai/wai-quantum-cli/Cargo.lock @@ -2,12 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -58,12 +73,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + [[package]] name = "arrayvec" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "base64" version = "0.23.1" @@ -104,6 +142,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + [[package]] name = "bytes" version = "1.12.1" @@ -188,6 +235,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -235,6 +291,15 @@ dependencies = [ "url", ] +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -253,6 +318,153 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80552083a2db143c5d951debe9f59a7d9f22084c70abe450fed5220060fa26c3" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf84b7e5f0aa42cb0d55cabf01c520fdb70a50d8b572a841ee875083589d643" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d47ec2358cdbace0520ac212c003656b529f0c60e43fbbe9a331632cd9e8fc" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebb94b54db96b26e387bc8c698823d83d9254d551a6ed846a60cebf62ed7135" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be81b89cb7b186a26cf2014c312aeb17737da988b4444d16a0cb3424d637d44e" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb1b64a509c3814932fb9845ce8da835cd10d62c6c56dacfdcd9af389d3bf313" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e667a0e9ed3bbbed0e7e52cd2dea85a645745b39f547097e2fca0bf317cac9df" + +[[package]] +name = "cranelift-control" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fa7d5123705226a368b6b1c7b10f23036c35e8041ffdd5f6505790f5c31404" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bffb83ac1d7052691d1710ca8eb334d4fcaad363e589f2b89d38d97bda5776" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc727c856bf57eae76c72ac4f9b1eaf49947a2b02f699798262fc8399557b7a" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e743f35915d55cd3d3178aab483661f78c2f9274aa704f29be69e3789c5bd" + +[[package]] +name = "cranelift-native" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127c50e5e2b2b97116d4e2c079db6ce29b07303cd1ab48a8bb4c2abbf16a7a68" +dependencies = [ + "cranelift-codegen", + "libc", + "mach2", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f34b169b8cbbaed2b4995205f1d02d7eac4b2771df5479255fe5483e1334a39" + [[package]] name = "crc32fast" version = "1.5.2" @@ -401,6 +613,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.3" @@ -414,7 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -440,6 +670,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -455,6 +691,70 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -487,11 +787,34 @@ dependencies = [ "r-efi", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "heck" @@ -626,7 +949,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -635,12 +960,27 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.191" @@ -657,6 +997,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -681,12 +1027,27 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "miniz_oxide" version = "0.9.1" @@ -739,6 +1100,18 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -776,6 +1149,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkcs8" version = "0.10.2" @@ -786,6 +1165,18 @@ dependencies = [ "spki", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -810,6 +1201,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c66a82a3d48db9b7c2ba1307e06fcb81a35ba82952f58bc1bfb218321f7c0b" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138a9a5997d9cba5e54c7c3b836d7164f2bfe0852c7e98d5835e1708d6efdf36" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "quote" version = "1.0.47" @@ -834,6 +1248,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + [[package]] name = "ring" version = "0.17.14" @@ -848,6 +1277,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -867,7 +1308,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -910,6 +1351,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -988,11 +1433,20 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] [[package]] name = "spki" @@ -1055,6 +1509,21 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -1237,6 +1706,7 @@ dependencies = [ "ureq", "wai-quantum", "wai-quantum-qpu", + "wai-wasmq", ] [[package]] @@ -1249,12 +1719,225 @@ dependencies = [ "wai-quantum", ] +[[package]] +name = "wai-wasmq" +version = "0.1.0" +dependencies = [ + "blake3", + "serde_json", + "wai-quantum", + "wasmparser", + "wasmtime", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a21c9e3325655ba02de3e645bc8357a8b3651a2e3ada25b6dbc37560deb9b7" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdfacce893df54d319b29720e79edb001cd1b48faaf78fdb3b9c608eaeeea29f" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79df81f2a5d7a1cb8b9eb35b4a2d7a8cb8e948bd3e5a1f79036c0094879275a" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c2836b127721e0abdc3490545cdc6d5269d1050a19a4057909901c16e1b781" + +[[package]] +name = "wasmtime-internal-core" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765a08b721bf2b2e5cbd3e9365a71b497d57fc5e8b2cf73ddf12d85d502d7fa" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e8f190dd62c021d278ab0bd4afab06426844831d6a35dc369835754ad997d4" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6326d47c759c0478489a034f1282a6f516ce97594f9af785539697d03add17ec" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b78d046468d9aaf81e97954165369693084f43e313daad47f959b1169d596d3" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e5f3cb3fdc12489163dc60741696dddb32c9ed0662ff59c8dfb177ffd7a07" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf9357720c1477c43f88dace11923737e8aa87abe5f46f90e23c93a2a0fb3fe" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce7c663ad018cc86ce824e21b35b8bed9877ee9ca6af8ef8672e28852450392e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "webpki-roots" version = "1.0.10" @@ -1280,6 +1963,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/wai/wai-quantum-cli/Cargo.toml b/wai/wai-quantum-cli/Cargo.toml index dfdf9dc0b..4a18eb9a9 100644 --- a/wai/wai-quantum-cli/Cargo.toml +++ b/wai/wai-quantum-cli/Cargo.toml @@ -19,12 +19,15 @@ name = "wai-quantum" path = "src/main.rs" [features] -default = ["service", "providers"] +default = ["service", "providers", "wasmq"] # `submit`: send a job to the quantum job service over HTTPS. service = ["dep:ureq"] # `providers` and `run --provider`: the person's own quantum providers, their # credentials in the operating system's keychain (wai-quantum-qpu). providers = ["dep:wai-quantum-qpu"] +# `pack-wasmq`, `check-wasmq`, and `run` and `verify` of a wasmQ envelope: an OpenQASM 3 +# program and the WebAssembly its `extern`s call, metered and receipted (wai-wasmq). +wasmq = ["dep:wai-wasmq"] # The single-precision state vector on the GPU (`--method statevector_single`), on macOS. metal = ["wai-quantum/quantum_sv_metal", "wai-quantum/quantum_cutrank_metal"] # The fabric's accelerator members a statevector job may be placed on (`--fabric`, `wai-quantum fabric`): each loads its @@ -43,3 +46,4 @@ serde_json = { version = "1", features = ["float_roundtrip"] } ureq = { version = "3", optional = true, features = ["json"] } wai-quantum = { version = "0.9.0", path = "../wai-quantum", default-features = false, features = ["full"] } wai-quantum-qpu = { version = "0.5.0", path = "../wai-quantum-qpu", optional = true } +wai-wasmq = { version = "0.1.0", path = "../wai-wasmq", optional = true } diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 978e6b92a..5947fd497 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -287,6 +287,36 @@ wai-quantum qec threshold --family color --distances 3,5,7 --p 0.02,0.05 `qec code` shows a code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72`, `bb:90`, `bb:108`, `bb:144` or `gross`, `bb:288`) with its distance certificate: in each sector, the least weight of a logical found, a witness of that weight, and every lighter support ruled out by an exhaustive search. `qec decode` decodes at code capacity (`matching`, `union_find`, `color`, `relay_bp`, `search`) or at circuit level (`matching`, `correlated`, `union_find`, `bp_osd`, `window`); a model alone can be decoded but not sampled, since it says how faults happen, not which did. `qec threshold` measures failure rates with their standard errors and fits the code model. Every answer is signed with your key (`--json` prints it sealed). The research forms stay at `wai-quantum lab qec`. What each decoder guarantees is on [the error-correction page](../wai-quantum/docs/qec.md). +## Programs that call WebAssembly between gates + +A program declares a classical function it calls in the middle of a circuit with +`extern`: a decoder between a syndrome and its correction, a planner between loading atoms +and moving them. `pack-wasmq` puts the program, the WebAssembly modules that define its +externs and the files those modules read into one envelope, each pinned by its digest +([wasmQ](../extensions/quantum-wasmq.md)). The files below are the conformance vectors' +run in the open ([`../quantum-conformance/wasmq`](../quantum-conformance/wasmq)): a memory +round of the distance-3 surface code, the reference decoder, and the parity checks it reads. + +```sh runnable copy=../quantum-conformance/wasmq +wai-quantum check-wasmq wasmq/modules/qldpc_decoder.wasm # the module against the profile: nothing imported, memory bounded +wai-quantum pack-wasmq wasmq/envelopes/surface3-decode64.qasm --callout decode64=wasmq/modules/qldpc_decoder.wasm \ + --prior wasmq/modules/qldpc_decoder.wasm=wasmq/envelopes/surface3-decode64.checks.bin --shots 1000 --fuel-per-call 200000 -o round.wai +wai-quantum run round.wai --demo-key --json > job.json # each call metered; the same bytes on every host +wai-quantum verify job.json --bundle round.wai # the signer, how the run ended, and the envelope run again and compared +``` + +This version runs Clifford programs: the gates that take no angle, measurement, reset and +conditions on bits. A call that runs past its fuel, recurses past the stack or traps ends +the run in a receipt that names the fault and still verifies. `verify` checks a wasmQ job +as it checks every job, and as every surface does (the service, the MCP servers, the Python +package, the app, a page): its seal, and its request and result held to the profile's form +(the reason `wasmq_form`), naming the signer and how the run ended; `--json` and `--lines` +give that verdict as JSON. `run --engine native|interpreter` chooses +the engine; the receipt is the same. `run --transcript FILE` records every call's inputs, +outputs and fuel, and `run round.wai --replay FILE` runs the modules alone on a record, +checks each call and states the record's digest, the one a receipt seals. The quantum part is simulated here: an envelope goes to no provider, +whose programs carry no call between gates. + ## Pulse programs A pulse program gives its gates as OpenPulse calibrations on physical qubits. It runs as a job of the method `pulse`: integrated under a master equation, its result the same bits on every platform, its error bounded and stated, and its receipt binding the control program, its calibrations, the device and the integrator. diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index 5e5c9ba8a..792711ec5 100644 --- a/wai/wai-quantum-cli/README.zh-CN.md +++ b/wai/wai-quantum-cli/README.zh-CN.md @@ -285,6 +285,20 @@ wai-quantum qec threshold --family color --distances 3,5,7 --p 0.02,0.05 `qec code` 显示一个码(`surface:D`、`toric:L`、`steane`、`color:D`、`bb:72`、`bb:90`、`bb:108`、`bb:144` 或 `gross`、`bb:288`)及其距离证书:在每个扇区中,找到的逻辑算符的最小权重、该权重的一个见证,以及经穷举搜索排除的每个更轻的支撑。`qec decode` 在码容量层面(`matching`、`union_find`、`color`、`relay_bp`、`search`)或线路层面(`matching`、`correlated`、`union_find`、`bp_osd`、`window`)解码;仅有模型时可以解码但不能采样,因为模型说明的是故障如何发生,而不是发生了哪些。`qec threshold` 测量失败率及其标准误差,并拟合码模型。每个回答都以你的密钥签名(`--json` 打印封存后的结果)。研究用的形式仍在 `wai-quantum lab qec`。各解码器保证什么,见[纠错页面](../wai-quantum/docs/qec.md)。 +## 在门之间调用 WebAssembly 的程序 + +程序用 `extern` 声明一个它在线路中途调用的经典函数:校验子与其纠正之间的解码器,装载原子与移动原子之间的规划器。`pack-wasmq` 把程序、定义其 extern 的 WebAssembly 模块以及这些模块读取的文件放进一个封装里,每一项都由其摘要固定([wasmQ](../extensions/quantum-wasmq.md))。下面的文件是一致性向量中公开的那次运行([`../quantum-conformance/wasmq`](../quantum-conformance/wasmq)):距离为 3 的表面码的一轮存储、参考解码器,以及它读取的奇偶校验。 + +```sh runnable copy=../quantum-conformance/wasmq +wai-quantum check-wasmq wasmq/modules/qldpc_decoder.wasm # 按规范检查模块:不导入任何东西,内存有界 +wai-quantum pack-wasmq wasmq/envelopes/surface3-decode64.qasm --callout decode64=wasmq/modules/qldpc_decoder.wasm \ + --prior wasmq/modules/qldpc_decoder.wasm=wasmq/envelopes/surface3-decode64.checks.bin --shots 1000 --fuel-per-call 200000 -o round.wai +wai-quantum run round.wai --demo-key --json > job.json # 每次调用都计量;在每台主机上字节相同 +wai-quantum verify job.json --bundle round.wai # 签名者、运行如何结束,以及重新运行封装并比较 +``` + +这一版本运行 Clifford 程序:不带角度的门、测量、重置和以比特为条件的操作。超出燃料、递归超出栈或发生陷阱的调用,会使运行以一份指明故障的回执结束,该回执仍可验证。`verify` 指明签名者以及运行如何结束。`run --engine native|interpreter` 选择引擎;回执相同。`run --transcript FILE` 记录每次调用的输入、输出和燃料,`run round.wai --replay FILE` 仅在一份记录上运行模块、检查每次调用,并给出该记录的摘要,即回执所封存的摘要。量子部分在此处模拟:封装不会发往任何提供方,提供方的程序不携带门之间的调用。 + ## 脉冲程序 脉冲程序以作用于物理量子比特的 OpenPulse 校准来给出它的门。它作为方法 `pulse` 的作业运行:在主方程下积分,结果在每个平台上比特一致,误差有界并予以说明,回执绑定控制程序、其校准、设备和积分器。 diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index d4b40b86b..17e0c521e 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -706,9 +706,11 @@ Parameters: Whether a result's receipt (an Ed25519 signature) seals exactly that request and that result, and every rule of its form holds (`reasons` names what does not). Pass the job's own `request` (as run_quantum_circuit or get_quantum_job returns it: a run here asks for its verdict, so the request it seals says so) and its result. Parameters: -- `job` (string): The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen) +- `job` (any): The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen), or the job as an object - `request` (object) - `result` (object) +- `status` (any): The body's status as the host gave it: unsealed, and noted where it differs from what is sealed +- `method` (any): The body's method as the host gave it: unsealed, and noted where it differs from what is sealed ### `critique_qasm`: Critique a quantum program @@ -1080,7 +1082,7 @@ Each construct below is supported (read as the language defines it), lowered (re |---|---|---| | `c = measure q;`, `c[0] = measure q[0];`, `bit c = measure q;` | supported | Q321 | | `measure q -> c;` | supported | Q321 | -| `measure q;` | supported where no bit register is declared | Q320 | +| `measure q;` | supported where no bit register is declared, and refused in a program that calls externs | Q320, Q324 | | Mid-circuit measurement, measurement again, a gate after it | lowered: the program runs as steps (counts by shot splitting, exact expectation values) | | | `reset q;` | supported: before a qubit's first gate it changes nothing; after it, the program runs as steps | | | `barrier`, `barrier q;` | carried in the timing | Q404 | @@ -1265,6 +1267,8 @@ Every code: | `Q320` | refused | A measurement whose result goes nowhere: `measure q;` while bit registers are declared (write `c = measure q;`). | | `Q321` | refused | Qubits measured into a different number of bits. | | `Q322` | lowered | A bit declared inside a block or a subroutine: a bit of its own, which the steps read and the results do not show. | +| `Q323` | refused | A program past a bound its reader holds it to as it reads it: qubits, bits, steps, the bits its calls of externs take and give, or the work of reading it (a reader of programs from others, as the hybrid profile reads them). | +| `Q324` | refused | A measurement with no target bit (`measure q;`) in a program that calls externs: its outcome is the bits it declares, so each measurement names the bit it writes (write `c = measure q;`). | | `Q400` | warned | The method applies no durations: the program's delays, boxes and stretches change nothing it computes, and the result says so. | | `Q401` | carried | `delay[d]` on qubits, carried in the program's timing. | | `Q402` | carried | `box` (with or without a duration), carried in the timing around the gates it holds. | @@ -1578,6 +1582,10 @@ Usage: wai-quantum [OPTIONS] Commands: run Run a program here, planned as the job service plans it, and sign the result + pack-wasmq Pack an OpenQASM 3 program and the WebAssembly modules its `extern`s call into one + wasmQ envelope, which `run` runs metered and receipted + check-wasmq Check a WebAssembly module against the wasmQ profile: nothing imported, memory + bounded, only what every engine runs alike plan Show the method a program would run on and why, without running it instance Write a generator out at its `--input` values, as plain OpenQASM convert Carry a program between formats (OpenQASM 3, QIR as LLVM text, jeff), every @@ -1874,6 +1882,126 @@ Options: With --provider: send the job and answer with its handle, without waiting (`wai-quantum providers collect`) + --engine + For a wasmQ envelope: the engine, `native` (the default where this build has it) or + `interpreter`; the receipt is the same + + --transcript + For a wasmQ envelope: write every callout's inputs, outputs and fuel here, one JSON object + a line + + --replay + For a wasmQ envelope: run only its modules, on the calls a transcript records (from a run + here, or from a machine that ran them), and check each call's output and fuel + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum pack-wasmq` + +```text +Pack an OpenQASM 3 program and the WebAssembly modules its `extern`s call into one wasmQ envelope, +which `run` runs metered and receipted + +Usage: wai-quantum pack-wasmq [OPTIONS] --callout --out + +Arguments: + + The OpenQASM 3 program: each `extern` it calls is a callout + +Options: + --callout + A callout: the `extern`'s name, the module that defines it, and the export if it is not + the same name (repeatable) + + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --prior + A prior a module takes, in the order given: the module's file and the prior's (repeatable) + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --shots + Shots to run + + [default: 1024] + + --seed + The seed the shots' measurements and the modules' seeds come from + + [default: 1] + + --state + `shot`: a fresh instance of each module every shot; `job`: one for the run, shots in order + + [default: shot] + + --fuel-per-call + The most fuel one callout may use + + [default: 10000000] + + --fuel-per-job + The most fuel the whole run may use: within what a run may be given, which its host's work + bounds (extensions/quantum-wasmq.md §4.8) + + [default: 17179869184] + + --fuel-per-instance + The most fuel an instance's seed, priors and init may use + + [default: 100000000] + + -o, --out + Where to write the envelope + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum check-wasmq` + +```text +Check a WebAssembly module against the wasmQ profile: nothing imported, memory bounded, only what +every engine runs alike + +Usage: wai-quantum check-wasmq [OPTIONS] + +Arguments: + + The module (.wasm) + +Options: + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --json + Print the check as JSON + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + -h, --help Print help @@ -3581,9 +3709,8 @@ Options: A value for a generator's integer `input`, as n=5 (repeat for each input): the program is read as its family's circuit at those values - --json - A job's verdict as JSON: valid, the reasons (wai/extensions/quantum-providers.md section - 8.4), the key, the receipt's form, and what a provider's result says of itself + --bundle + For a wasmQ job: the envelope it ran, which is run again here and compared byte for byte --lang The language of the words the command line writes itself, en or zh-CN (by default the one @@ -3593,6 +3720,10 @@ Options: The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + --json + A job's verdict as JSON: valid, the reasons (wai/extensions/quantum-providers.md section + 8.4), the key, the receipt's form, and what a provider's result says of itself + --lines One job a line, each judged as a job's receipt, one JSON verdict a line; the status is 1 when any is not valid diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index b4c691083..83bc58aac 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21557,10 +21557,10 @@ "outputSchema": { "$id": "wai-quantum://schemas/verify_quantum_result/1", "type": "object", - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here; for a wasmQ job, also its body and its form (extensions/quantum-wasmq.md §6.5).", "anyOf": [ { - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here; for a wasmQ job, also its body and its form (extensions/quantum-wasmq.md §6.5).", "anyOf": [ { "type": "object", @@ -21578,7 +21578,7 @@ "items": { "type": "string" }, - "description": "Each rule the receipt breaks (the provider protocol's section 8.4): none when it is valid." + "description": "Each rule the receipt breaks (the provider protocol's section 8.4, wasmq_form among them for a wasmQ job): none when it is valid." }, "form": { "type": [ @@ -21604,6 +21604,50 @@ "null" ], "description": "That key as a did:key." + }, + "wasmq": { + "type": "object", + "description": "For a wasmQ job (quantum-wasmq section 6.5), beside the verdict: how the run ended, each rule of the profile's form it breaks, and where the host's unsealed words differ from what is sealed, which change no verdict.", + "required": [ + "outcome", + "problems", + "notes" + ], + "additionalProperties": false, + "properties": { + "outcome": { + "type": [ + "string", + "null" + ], + "description": "The sealed result's status: ok, or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Each rule of quantum-wasmq section 6.5 the job breaks, in words (the reason wasmq_form): the first 64, then how many more. Empty when it breaks none." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the body's unsealed status or method differs from what is sealed or is missing, and members nothing seals: told beside the verdict, and no part of it." + }, + "bundle": { + "type": "boolean", + "description": "`wai-quantum verify --json --bundle` only: whether the envelope given is the one the receipt binds." + }, + "reproduced": { + "type": [ + "boolean", + "null" + ], + "description": "`wai-quantum verify --json --bundle` only: whether running the envelope again here gives the same result, byte for byte; null when it is not the envelope the receipt binds." + } + } } } }, @@ -21679,6 +21723,13 @@ "valid": { "type": "boolean" }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The provider protocol's section 8.4 reasons: none, since an answer is run again only once its receipt and every rule hold; a receipt that fails is answered with its reasons alone." + }, "signed": { "type": "boolean" }, @@ -21688,6 +21739,13 @@ "reproduced": { "type": "boolean" }, + "rerun_error": { + "type": [ + "string", + "null" + ], + "description": "Why the request could not be run again, when no host runs it (a request an estimate refuses): the answer is then not valid, and nothing is reproduced. Null when it ran." + }, "key": { "type": [ "string", @@ -21719,6 +21777,13 @@ "valid": { "type": "boolean" }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The provider protocol's section 8.4 reasons: none, since an answer is run again only once its receipt and every rule hold; a receipt that fails is answered with its reasons alone." + }, "signed": { "type": "boolean" }, diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 98e4a5cc6..1b10e5b69 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -31811,14 +31811,23 @@ "type": "object", "properties": { "job": { - "type": "string", - "description": "The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen)" + "type": [ + "string", + "object" + ], + "description": "The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen), or the job as an object" }, "request": { "type": "object" }, "result": { "type": "object" + }, + "status": { + "description": "The body's status as the host gave it: unsealed, and noted where it differs from what is sealed" + }, + "method": { + "description": "The body's method as the host gave it: unsealed, and noted where it differs from what is sealed" } } }, @@ -31848,7 +31857,7 @@ "items": { "type": "string" }, - "description": "Each rule the receipt breaks (the provider protocol's section 8.4): none when it is valid." + "description": "Each rule the receipt breaks (the provider protocol's section 8.4, wasmq_form among them for a wasmQ job): none when it is valid." }, "form": { "type": [ @@ -31873,6 +31882,39 @@ "null" ], "description": "That key as a did:key." + }, + "wasmq": { + "type": "object", + "description": "For a wasmQ job (quantum-wasmq section 6.5), beside the verdict: how the run ended, each rule of the profile's form it breaks, and where the host's unsealed words differ from what is sealed, which change no verdict.", + "required": [ + "outcome", + "problems", + "notes" + ], + "additionalProperties": false, + "properties": { + "outcome": { + "type": [ + "string", + "null" + ], + "description": "The sealed result's status: ok, or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Each rule of quantum-wasmq section 6.5 the job breaks, in words (the reason wasmq_form): the first 64, then how many more. Empty when it breaks none." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the body's unsealed status or method differs from what is sealed or is missing, and members nothing seals: told beside the verdict, and no part of it." + } + } } } }, @@ -31889,6 +31931,13 @@ "valid": { "type": "boolean" }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The provider protocol's section 8.4 reasons: none, since an answer is run again only once its receipt and every rule hold; a receipt that fails is answered with its reasons alone." + }, "signed": { "type": "boolean" }, @@ -31898,6 +31947,13 @@ "reproduced": { "type": "boolean" }, + "rerun_error": { + "type": [ + "string", + "null" + ], + "description": "Why the request could not be run again, when no host runs it (a request an estimate refuses): the answer is then not valid, and nothing is reproduced. Null when it ran." + }, "key": { "type": [ "string", diff --git a/wai/wai-quantum-cli/locales/zh-CN.json b/wai/wai-quantum-cli/locales/zh-CN.json index 449d5d73b..072d64788 100644 --- a/wai/wai-quantum-cli/locales/zh-CN.json +++ b/wai/wai-quantum-cli/locales/zh-CN.json @@ -35,6 +35,8 @@ "help": { "_root": { "source": "OpenQASM programs run here as the quantum job service runs them, or sent to it; every result names its method and accuracy and is signed", "text": "OpenQASM 程序在本机按量子作业服务(job service)的方式运行,或发送给该服务运行;每个结果都注明其方法和精度,并附有签名" }, "run": { "source": "Run a program here, planned as the job service plans it, and sign the result", "text": "在本机运行程序,按作业服务的方式规划,并为结果签名" }, + "pack-wasmq": { "source": "Pack an OpenQASM 3 program and the WebAssembly modules its `extern`s call into one wasmQ envelope, which `run` runs metered and receipted", "text": "将 OpenQASM 3 程序及其 `extern` 所调用的 WebAssembly 模块打包为一个 wasmQ 信封,由 `run` 计量运行并出具回执" }, + "check-wasmq": { "source": "Check a WebAssembly module against the wasmQ profile: nothing imported, memory bounded, only what every engine runs alike", "text": "按 wasmQ 规范检查 WebAssembly 模块:不导入任何内容,内存有界,只使用每个引擎都以同样方式运行的部分" }, "plan": { "source": "Show the method a program would run on and why, without running it", "text": "显示程序将在哪种方法上运行及其原因,而不运行它" }, "instance": { "source": "Write a generator out at its `--input` values, as plain OpenQASM", "text": "按 `--input` 给出的取值,把生成器写成普通的 OpenQASM" }, "convert": { "source": "Carry a program between formats (OpenQASM 3, QIR as LLVM text, jeff), every conversion checked: what is written is read back and held to the program by the equivalence ladder; notes and the check go to standard error (exit 1 refused, 2 the check found it different)", "text": "在格式之间转换程序(OpenQASM 3、以 LLVM 文本表示的 QIR、jeff),每次转换都经过检查:写出的结果会被读回,并由等价判定阶梯与原程序比对;说明和检查结果输出到标准错误(退出状态 1 表示被拒绝,2 表示检查发现两者不同)" }, diff --git a/wai/wai-quantum-cli/schemas/local-tools.v1.json b/wai/wai-quantum-cli/schemas/local-tools.v1.json index 7ea23dbf6..1357f76e6 100644 --- a/wai/wai-quantum-cli/schemas/local-tools.v1.json +++ b/wai/wai-quantum-cli/schemas/local-tools.v1.json @@ -5,7 +5,7 @@ "description": "The answers of the local MCP server's own tools (wai-quantum mcp), those the command line writes beside the engine's: a receipt's check, the workbench archive's submissions, a program's history, the person's taste, and the person's own providers. Each is a $def; every tool's outputSchema in tools/list is its answer's schema standing alone. The engine's answers are in wai-quantum's schemas.", "$defs": { "verify": { - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here; for a wasmQ job, also its body and its form (extensions/quantum-wasmq.md §6.5).", "anyOf": [ { "type": "object", @@ -23,7 +23,7 @@ "items": { "type": "string" }, - "description": "Each rule the receipt breaks (the provider protocol's section 8.4): none when it is valid." + "description": "Each rule the receipt breaks (the provider protocol's section 8.4, wasmq_form among them for a wasmQ job): none when it is valid." }, "form": { "type": [ @@ -49,6 +49,50 @@ "null" ], "description": "That key as a did:key." + }, + "wasmq": { + "type": "object", + "description": "For a wasmQ job (quantum-wasmq section 6.5), beside the verdict: how the run ended, each rule of the profile's form it breaks, and where the host's unsealed words differ from what is sealed, which change no verdict.", + "required": [ + "outcome", + "problems", + "notes" + ], + "additionalProperties": false, + "properties": { + "outcome": { + "type": [ + "string", + "null" + ], + "description": "The sealed result's status: ok, or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Each rule of quantum-wasmq section 6.5 the job breaks, in words (the reason wasmq_form): the first 64, then how many more. Empty when it breaks none." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the body's unsealed status or method differs from what is sealed or is missing, and members nothing seals: told beside the verdict, and no part of it." + }, + "bundle": { + "type": "boolean", + "description": "`wai-quantum verify --json --bundle` only: whether the envelope given is the one the receipt binds." + }, + "reproduced": { + "type": [ + "boolean", + "null" + ], + "description": "`wai-quantum verify --json --bundle` only: whether running the envelope again here gives the same result, byte for byte; null when it is not the envelope the receipt binds." + } + } } } }, diff --git a/wai/wai-quantum-cli/src/estimate.rs b/wai/wai-quantum-cli/src/estimate.rs index e753178e0..36b7bae3c 100644 --- a/wai/wai-quantum-cli/src/estimate.rs +++ b/wai/wai-quantum-cli/src/estimate.rs @@ -216,7 +216,10 @@ pub fn verify(body: &Value) -> bool { println!("signature {}", if yes("signed") { "VALID" } else { "INVALID" }); println!("binds the request and the result as given"); println!("receipts {} estimates, each a resource-estimate receipt: {}", v["estimates"], if yes("receipts") { "signed by the same key, the frontier's naming the headline's, each re-run here to the same bits" } else { "NOT ALL HOLD" }); - println!("reproduced {}", if yes("reproduced") { "the request re-run here gives every count and figure" } else { "NO: the request re-run here gives other figures" }); + match v["rerun_error"].as_str() { + Some(e) => println!("reproduced NO: the request is not one an estimate runs: {e}"), + None => println!("reproduced {}", if yes("reproduced") { "the request re-run here gives every count and figure" } else { "NO: the request re-run here gives other figures" }), + } if let Some(d) = v["signer"].as_str() { println!("signer {d}"); } diff --git a/wai/wai-quantum-cli/src/lab.rs b/wai/wai-quantum-cli/src/lab.rs index 37be92be7..86089cdd9 100644 --- a/wai/wai-quantum-cli/src/lab.rs +++ b/wai/wai-quantum-cli/src/lab.rs @@ -459,6 +459,12 @@ fn estimate_json(e: &qr::Estimate) -> String { /// Check a receipt of any kind this tool or the library signs: `src` is its /// JSON. Prints what it binds, and exits 1 when it does not verify. +/// Whether `src` is a receipt of the research tools' (a resource estimate's, sparse Pauli +/// dynamics', a state's), which [`verify_receipt`] reads: no job's. +pub fn is_receipt(src: &str) -> bool { + MaybeLabelled::::from_json(src).is_some() || MaybeLabelled::::from_json(src).is_some() || MaybeLabelled::::from_json(src).is_some() +} + pub fn verify_receipt(src: &str) { // Either form: a receipt as sealed, or one whose joule figure // carries its acquisition class inside the signature diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 225b550ea..b4896904d 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -66,6 +66,9 @@ //! wai-quantum run rabi.qasm --method pulse # a pulse program (OpenPulse) integrated, its error bounded, the same bits everywhere //! wai-quantum pulse schedule rabi.qasm # its frames' timelines, plays, waveforms and gate calls //! wai-quantum pulse check job.json # a pulse job's receipt, its program, and the run again, bit for bit +//! wai-quantum pack-wasmq round.qasm --callout decode64=decoder.wasm -o round.wai # a program and the WebAssembly its externs call, one envelope +//! wai-quantum run round.wai --json # each callout metered, the same bytes on every host, the job receipted +//! wai-quantum verify job.json --bundle round.wai # the receipt, and the envelope run again and compared //! wai-quantum landscape run prog.epuml # an energy-landscape job: the answer, every arrangement checked where small, the priced joules //! wai-quantum landscape compare prog.epuml # the same job on the emulator and on classical methods, at the same budget //! wai-quantum completions zsh # shell completion @@ -117,6 +120,8 @@ mod taste; mod variational; #[cfg(feature = "service")] mod service; +#[cfg(feature = "wasmq")] +mod wasmq; #[cfg(feature = "service")] mod task; @@ -161,6 +166,21 @@ enum Command { #[cfg(feature = "providers")] #[command(flatten)] provider: providers::RunArgs, + #[cfg(feature = "wasmq")] + #[command(flatten)] + wasmq: wasmq::RunArgs, + }, + /// Pack an OpenQASM 3 program and the WebAssembly modules its `extern`s call into one wasmQ envelope, which `run` runs metered and receipted + #[cfg(feature = "wasmq")] + PackWasmq(wasmq::PackArgs), + /// Check a WebAssembly module against the wasmQ profile: nothing imported, memory bounded, only what every engine runs alike + #[cfg(feature = "wasmq")] + CheckWasmq { + /// The module (.wasm) + module: PathBuf, + /// Print the check as JSON + #[arg(long)] + json: bool, }, /// Show the method a program would run on and why, without running it Plan { @@ -312,11 +332,14 @@ enum Command { /// Also check that the job carries this history's account of its program (`run --history`) #[arg(long, value_name = "FILE")] history: Option, + /// For a wasmQ job: the envelope it ran, which is run again here and compared byte for byte + #[arg(long, value_name = "FILE")] + bundle: Option, /// A job's verdict as JSON: valid, the reasons (wai/extensions/quantum-providers.md section 8.4), the key, the receipt's form, and what a provider's result says of itself #[arg(long)] json: bool, /// One job a line, each judged as a job's receipt, one JSON verdict a line; the status is 1 when any is not valid - #[arg(long, conflicts_with = "history")] + #[arg(long, conflicts_with_all = ["history", "bundle"])] lines: bool, }, /// Show the key this user signs receipts with @@ -566,6 +589,32 @@ struct Job { fabric: Vec, } +/// The first flag of a program's run that is given: an envelope has no use for any. +#[cfg(feature = "wasmq")] +fn for_a_program(job: &Job) -> Option<&'static str> { + [ + (!job.observables.is_empty(), "--observable"), + (!job.amplitudes.is_empty(), "--amplitude"), + (!job.noise.is_empty(), "--noise"), + (!job.options.is_empty(), "--option"), + (job.history.is_some(), "--history"), + (job.compile.is_some(), "--compile"), + (job.target.is_some(), "--target"), + (!job.intent.is_empty(), "--intent"), + (job.verdict, "--verdict"), + (job.repair, "--repair"), + (job.max_iterations.is_some(), "--max-iterations"), + (!job.programs.is_empty(), "--program"), + (!job.sweep.is_empty(), "--sweep"), + (job.mitigate.is_some(), "--mitigate"), + (job.pulse.is_some(), "--pulse"), + (!job.params.is_empty(), "--param"), + (!job.fabric.is_empty(), "--fabric"), + ] + .into_iter() + .find_map(|(given, flag)| given.then_some(flag)) +} + /// `name=v1,v2,…`: each value a whole number if it reads as one, else a number. fn sweep_arg(s: &str) -> Result<(String, Vec), String> { let (k, v) = s.split_once('=').ok_or("a sweep is NAME=V1,V2,…")?; @@ -740,7 +789,41 @@ fn at_inputs(text: String, inputs: &wai_quantum::qasm::Inputs) -> Result) -> Result>, String> { + use std::io::Read as _; + let most = JOB_BOUND.bytes; + let bytes = match file { + Some(p) if p != Path::new("-") => { + let f = std::fs::File::open(p).map_err(|e| format!("{}: {e}", p.display()))?; + let len = f.metadata().map_err(|e| format!("{}: {e}", p.display()))?.len(); + if len > most { + return Ok(None); + } + // Its length, held once: no doubling of what is read on a 32-bit host. + let mut b = Vec::with_capacity(len as usize + 1); + f.take(most + 1).read_to_end(&mut b).map_err(|e| format!("{}: {e}", p.display()))?; + b + } + _ => { + let mut b = Vec::new(); + std::io::stdin().take(most + 1).read_to_end(&mut b).map_err(|e| format!("standard input: {e}"))?; + b + } + }; + Ok((bytes.len() as u64 <= most).then_some(bytes)) +} + fn read_text(file: Option<&Path>) -> Result { match file { Some(p) if p != Path::new("-") => std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display())), @@ -1187,8 +1270,27 @@ fn main() { let _ = INPUTS.set(cli.inputs.iter().cloned().collect()); let _ = FROM.set(cli.from); match cli.command { + #[cfg(all(feature = "wasmq", feature = "providers"))] + Command::Run { job, provider, .. } if provider.provider.is_some() && wasmq::is_bundle(job.file.as_deref()) => die(wasmq::NO_PROVIDER), + #[cfg(feature = "wasmq")] + Command::Run { job, out, key, wasmq, .. } if wasmq::is_bundle(job.file.as_deref()) => { + if job.shots.is_some() || job.seed.is_some() || job.method.is_some() { + die("a wasmQ envelope states its own shots and seed, which `pack-wasmq --shots N --seed S` writes into it, and runs the one method its profile has, stabilizer"); + } + if let Some(flag) = for_a_program(&job) { + die(&format!("{flag} is for a program's run: a wasmQ envelope runs as its contract says, and nothing else is asked of it")); + } + let k = load_key(&key); + wasmq::run(job.file.as_deref().unwrap_or(Path::new("-")), out.json, out.top, &k.key, &wasmq) + } + #[cfg(feature = "wasmq")] + Command::PackWasmq(a) => wasmq::pack(&a, INPUTS.get().cloned().unwrap_or_default()), + #[cfg(feature = "wasmq")] + Command::CheckWasmq { module, json } => wasmq::check(&module, json), + #[cfg(feature = "wasmq")] + Command::Run { wasmq, .. } if wasmq.given().is_some() => die(&format!("{} is for a wasmQ envelope (`pack-wasmq`), not a program", wasmq.given().unwrap_or_default())), #[cfg(feature = "providers")] - Command::Run { job, out, key, provider } if provider.provider.is_some() => providers::run(&job, &out, &key, &provider), + Command::Run { job, out, key, provider, .. } if provider.provider.is_some() => providers::run(&job, &out, &key, &provider), Command::Run { job, out, key, .. } => { let req = job_request(&job).unwrap_or_else(|e| die(&e)); let k = load_key(&key); @@ -1399,24 +1501,35 @@ fn main() { } #[cfg(feature = "service")] Command::Task { cmd, args } => task::main(cmd, &args), - Command::Verify { file, history: hist, json: as_json, lines } => { - let bytes = read_bytes(file.as_deref()).unwrap_or_else(|e| die(&e)); - let src = match String::from_utf8(bytes) { - Ok(s) => s, - Err(e) => { - // Not UTF-8: unreadable, as a verifier reads a job (section 8.2). - if as_json || lines { - println!("{}", job::check_bytes(e.as_bytes()).to_json()); - } else { - println!("signature INVALID (unreadable: the text is not UTF-8)"); - } - std::process::exit(1); + Command::Verify { file, history: hist, bundle, json: as_json, lines } => { + let Some(bytes) = read_job_bytes(file.as_deref()).unwrap_or_else(|e| die(&e)) else { + // Past what a job holds: a file of lines is refused by its size, and a job is + // `too_large`, reported alone (section 8.4), before either is read. + if lines { + die(&format!("the file holds more than the {} bytes verify reads", JOB_BOUND.bytes)); } + say_too_large(as_json, bundle.is_some()); }; if lines { + // One verdict a line, each line read on its own as a job's text (section + // 8.2): a line that is not UTF-8, or blank, is that line's `unreadable`, and + // the newline that ends the file ends its last line. + let mut each: Vec<&[u8]> = bytes.split(|&b| b == b'\n').collect(); + if each.last().is_some_and(|l| l.is_empty()) { + each.pop(); + } + if each.is_empty() { + die("no job in the file: --lines reads one job a line"); + } let mut all = true; - for line in src.lines().filter(|l| !l.trim().is_empty()) { - let c = job::check_text(line); + for line in each { + // A line past the values verify reads is too_large, as a file is. + if !job::within(line, JOB_BOUND) { + all = false; + println!("{}", job::too_large(JOB_BOUND)); + continue; + } + let c = job::check_bytes(line); all &= c.valid(); println!("{}", c.to_json()); } @@ -1425,101 +1538,118 @@ fn main() { } return; } - let Ok(v) = serde_json::from_str::(&src) else { - // Not JSON at all: a job's verdict says so, as every reader must. - let c = job::check_text(&src); - if as_json { - println!("{}", c.to_json()); - std::process::exit(1); + if !job::within(&bytes, JOB_BOUND) { + say_too_large(as_json, bundle.is_some()); + } + let src = match String::from_utf8(bytes) { + Ok(s) => s, + Err(e) => { + // Not UTF-8: unreadable, as a verifier reads a job (section 8.2). + let c = job::check_bytes(e.as_bytes()); + say_alone(&c, as_json, bundle.is_some()); } - die("not JSON"); }; + let (c, parsed) = job::checked_text(&src); + // A lab receipt (a resource estimate's, sparse Pauli dynamics', a state's) is no + // job: its own reader checks it, as it did before there were jobs. + if !as_json && hist.is_none() && bundle.is_none() && c.reasons.first() == Some(&job::NOT_A_JOB) && lab::is_receipt(&src) { + lab::verify_receipt(&src); + return; + } + // Text that does not read, a body that is no job and a result with no receipt are + // each that reason alone (section 8.4), whatever kind of job the body says it is. + if job::reported_alone(&c) { + say_alone(&c, as_json, bundle.is_some()); + } + let v: Value = parsed.unwrap_or_else(|| die("not JSON")); // A set, an estimate and an algorithm's answer each have a verifier of - // their own, over what was read: their text is read as every job's is. - let own_verifier = v["result"]["format"] == wai_quantum::engine::estimate::FORMAT || v["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS; + // their own, run once the receipt and every rule of section 8.4 hold. + // A wasmQ job is read and judged as every job is (job::check holds it to its + // profile's form, whatever else its result says it is); only its envelope, run + // again here, is its own to check. + let wasmq = wai_quantum::engine::wasmq::is_job(&v["result"]); + let own_verifier = job::has_own_verifier(&v); let set = v["result"]["format"] == wai_quantum::engine::sets::FORMAT; + if wasmq && hist.is_some() { + die("--history checks a run against its history; a wasmQ job carries none, so nothing could be checked"); + } + if bundle.is_some() && !wasmq { + die("--bundle is for a wasmQ job (`run ENVELOPE.wai --json`)"); + } + #[cfg(not(feature = "wasmq"))] + if bundle.is_some() { + die("this build runs no wasmQ envelope: --bundle needs the feature wasmq"); + } if set && hist.is_some() { die("--history checks a run against its history; this is a set, which has none to check"); } - if own_verifier { - if hist.is_some() { - die("--history checks a run against its history; this is a set, an estimate or an algorithm's answer, which has none to check"); - } - if let Some(reason) = job::text_refusal(&src) { - if as_json { - println!("{}", json!({ "valid": false, "reasons": [reason] })); - } else { - println!("rules BROKEN: {reason}"); - } - std::process::exit(1); - } + if own_verifier && hist.is_some() { + die("--history checks a run against its history; this is an estimate or an algorithm's answer, which has none to check"); + } + if own_verifier && c.valid() { + // The receipt holds: the request is run again here (job::rerun). if as_json { - let verdict = if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - wai_quantum::engine::estimate::verify(&v).unwrap_or_else(|e| die(&e)) - } else { - wai_quantum::engine::variational::verify(&v) - }; + let verdict = job::rerun(&v).unwrap_or_else(|e| die(&e)); println!("{verdict}"); if verdict["valid"] != true { std::process::exit(1); } return; } - } - if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - if !estimate::verify(&v) { - std::process::exit(1); - } - } else if v["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { - if !variational::verify(&v) { - std::process::exit(1); - } - } else if as_json || (v.get("request").is_some() && v.get("result").is_some()) { - let c = job::check_text(&src); - if as_json { - // A set's verdict also says what the engine checks past its receipt. - let mut said = if set { job::verdict_of_text(&src).unwrap_or_else(|_| c.to_json()) } else { c.to_json() }; - if let Some(p) = &hist { - let h = history::load(p).unwrap_or_else(|e| die(&e)); - let r = h.check_job(&v).unwrap_or_else(|e| die(&e)); - if r["ok"] != true { - said["valid"] = json!(false); - } - said["history"] = r; - } - println!("{said}"); - if said["valid"] != true { - std::process::exit(1); - } - return; - } - let mut ok = c.valid(); - print_check(&c); - if set { - let s = wai_quantum::engine::sets::verify(&v); - let yes = |b: &Value| if *b == true { "yes" } else { "NO" }; - println!("set its receipt over its request and head, its root over {} items", s["items"]); - println!("items as the request gives them (each program written out again here): {}; each sealed by its own receipt and its path: {}; the root: {}", yes(&s["derived"]), yes(&s["items_sealed"]), yes(&s["root"])); - ok &= s["derived"] == true; - } - if let Some(c) = v["request"].get("history") { - let hands: Vec = c["gates"].as_object().into_iter().flatten().map(|(h, n)| format!("{n} {}", h.replace('_', " "))).collect(); - println!("history {}: {} of {} gates", c["version"].as_str().unwrap_or("?"), hands.join(", "), c["of"]["gates"]); - } - if let Some(p) = hist { - let h = history::load(&p).unwrap_or_else(|e| die(&e)); - let r = h.check_job(&v).unwrap_or_else(|e| die(&e)); - println!("account {}", if r["ok"] == true { "the history's own, recomputed from it".to_string() } else { format!("NOT the history's: {}", r["problems"]) }); - ok &= r["ok"] == true; - } + let ok = if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { estimate::verify(&v) } else { variational::verify(&v) }; if !ok { std::process::exit(1); } - } else { - if hist.is_some() { - die("--history checks a run against its history; this is no run's receipt"); + return; + } + if as_json { + // A set's verdict also says what the engine checks past its receipt. + let mut said = if set { job::verdict_of_checked(&v, &c) } else { c.to_json() }; + if let Some(p) = &hist { + let h = history::load(p).unwrap_or_else(|e| die(&e)); + let r = h.check_job(&v).unwrap_or_else(|e| die(&e)); + if r["ok"] != true { + said["valid"] = json!(false); + } + said["history"] = r; } - lab::verify_receipt(&src); + #[cfg(feature = "wasmq")] + if let Some(b) = &bundle + && c.wasmq.is_some() + { + wasmq::bundle_into(&v, b, &mut said); + } + println!("{said}"); + if said["valid"] != true { + std::process::exit(1); + } + return; + } + let mut ok = c.valid(); + print_check(&c); + #[cfg(feature = "wasmq")] + if wasmq { + ok &= wasmq::report(&c, &v, bundle.as_deref()); + } + if set { + let s = wai_quantum::engine::sets::verify(&v); + let yes = |b: &Value| if *b == true { "yes" } else { "NO" }; + println!("set its receipt over its request and head, its root over {} items", s["items"]); + println!("items as the request gives them (each program written out again here): {}; each sealed by its own receipt and its path: {}; the root: {}", yes(&s["derived"]), yes(&s["items_sealed"]), yes(&s["root"])); + ok &= s["derived"] == true; + } + if let Some(c) = v["request"].get("history") { + let hands: Vec = c["gates"].as_object().into_iter().flatten().map(|(h, n)| format!("{n} {}", h.replace('_', " "))).collect(); + println!("history {}: {} of {} gates", c["version"].as_str().unwrap_or("?"), hands.join(", "), c["of"]["gates"]); + } + if let Some(p) = hist { + let h = history::load(&p).unwrap_or_else(|e| die(&e)); + let r = h.check_job(&v).unwrap_or_else(|e| die(&e)); + println!("account {}", if r["ok"] == true { "the history's own, recomputed from it".to_string() } else { format!("NOT the history's: {}", r["problems"]) }); + ok &= r["ok"] == true; + } + if !ok { + std::process::exit(1); } } Command::Key { key } => { @@ -1635,6 +1765,45 @@ fn main() { /// A job's verdict for a person: the seal, what it binds, the signer and the /// receipt's form, the rules, and what a provider's result says of itself. +/// A job refused before its form was read (section 8.4: reported alone): its verdict, or +/// the check's lines and that its form was not judged; an envelope is not run for it. +/// A job past what `verify` reads: `too_large`, reported alone (section 8.4). +fn say_too_large(as_json: bool, bundle: bool) -> ! { + if as_json { + println!("{}", job::too_large(JOB_BOUND)); + } else { + println!("signature INVALID (too_large)"); + not_judged(job::TOO_LARGE, bundle); + } + std::process::exit(1) +} + +fn say_alone(c: &job::Check, as_json: bool, bundle: bool) -> ! { + if as_json { + println!("{}", c.to_json()); + } else { + print_check(c); + not_judged(c.reasons.first().copied().unwrap_or(""), bundle); + } + std::process::exit(1) +} + +/// That a job's form was not judged, and why, as `verify` prints it. +pub(crate) fn not_judged(reason: &str, bundle: bool) { + let why = match reason { + job::UNREADABLE => "its text is unreadable", + job::NO_RECEIPT => "its result carries no receipt", + job::NOT_A_JOB => "it is not a job", + job::TOO_LARGE => "it is longer than the 384 MiB, or holds more than the 2^22 values, verify reads", + "" => "it was not read", + other => other, + }; + println!("result not judged: {why}, which is reported alone (the provider protocol's section 8.4)"); + if bundle { + println!("bundle not run: the job was not judged"); + } +} + fn print_check(c: &job::Check) { use wai_quantum::engine::receipt::{BAD_KEY, BAD_SIGNATURE, DIGEST_MISMATCH, RECEIPT_FORM}; let seal = [job::UNREADABLE, job::NOT_A_JOB, job::NO_RECEIPT, RECEIPT_FORM, DIGEST_MISMATCH, BAD_KEY, BAD_SIGNATURE]; diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index d92532e9e..24864922d 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -620,28 +620,20 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli f => Err(format!("{f}: draw_circuit's formats are text, svg, latex and description")), } } - "verify_quantum_result" => { + VERIFY_TOOL => { let given = args.get("job").ok_or("give \"job\": {\"request\": ..., \"result\": ...}, or the job's text")?; - // Its text, read as the protocol's section 8.2 reads it: what a parsed - // job no longer shows (a member named twice, a number past what it keeps) is seen. - if let Some(text) = given.as_str() { - // Text that does not read is a verdict (unreadable), not a failure of the tool. - let parsed: Value = serde_json::from_str(text).unwrap_or(Value::Null); - if parsed["result"]["format"] == wai_quantum::engine::estimate::FORMAT || parsed["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { - if let Some(reason) = job::text_refusal(text) { - return Ok(json!({ "valid": false, "reasons": [reason] })); - } - return if parsed["result"]["format"] == wai_quantum::engine::estimate::FORMAT { wai_quantum::engine::estimate::verify(&parsed) } else { Ok(wai_quantum::engine::variational::verify(&parsed)) }; - } - return job::verdict_of_text(text).or_else(|_| Ok(job::verdict_of(&job::check_text(text)))); + // Its text, read as the protocol's section 8.2 reads it: what a parsed job no longer + // shows (a member named twice, a number past what it keeps) is seen. Every body + // gets the answer every surface gives it (job::answer_of): a verdict, and for an + // estimate or an algorithm's answer whose receipt holds, its request run again. + let answer = match given.as_str() { + Some(text) => job::answer_of_text(text), + None => job::answer_of(given), + }; + match answer { + job::Answer::Verdict(v) => Ok(v), + job::Answer::Rerun(j) => job::rerun(&j), } - if given["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - return wai_quantum::engine::estimate::verify(given); - } - if given["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { - return Ok(wai_quantum::engine::variational::verify(given)); - } - job::verdict(given) } "list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })), "list_fabric" => Ok(wai_quantum::engine::fabric::list(memory_gb << 30)), @@ -1001,18 +993,77 @@ fn task_method(method: &str, params: &Value, id: &Value, key: &SigningKey, clien } } -/// Serve until standard input closes. +/// The most bytes of a message line this server reads whole: a job within what `verify` +/// reads ([`crate::JOB_BOUND`]) given as a string, each byte written at most six +/// (`\u00XX`), and the call around it; on a 32-bit host, 1 GiB. Past it a line is read +/// only as far as its id, method and tool, and the rest of it passed over. +#[cfg(not(target_pointer_width = "32"))] +const LINE_BYTES: usize = 6 * crate::JOB_BOUND.bytes as usize + (1 << 20); +#[cfg(target_pointer_width = "32")] +const LINE_BYTES: usize = 1 << 30; + +/// One line of `r`, its newline taken off, held to at most `cap` bytes: the bytes, and +/// whether the line went on past them. `None` at the end of the input. +fn read_line_at_most(r: &mut impl BufRead, cap: usize) -> std::io::Result, bool)>> { + let (mut line, mut cut, mut any) = (Vec::new(), false, false); + loop { + let buf = r.fill_buf()?; + if buf.is_empty() { + return Ok(any.then_some((line, cut))); + } + any = true; + let (take, end) = match buf.iter().position(|&b| b == b'\n') { + Some(i) => (i, true), + None => (buf.len(), false), + }; + let room = cap.saturating_sub(line.len()); + line.extend_from_slice(&buf[..take.min(room)]); + cut |= take > room; + r.consume(take + usize::from(end)); + if end { + return Ok(Some((line, cut))); + } + } +} + +/// A call answered before its message is read whole: the verify tool's verdict of a job past +/// what this server reads, or nested past what a reader takes; any other call is refused, +/// by its id. +fn judged(msg: &Value, verdict: Value, views: bool) -> Option { + let id = msg.get("id")?.clone(); + Some(if msg["method"] == "tools/call" && msg["params"]["name"] == VERIFY_TOOL { + json!({ "jsonrpc": "2.0", "id": id, "result": answer(VERIFY_TOOL, &json!({}), Ok(verdict), views) }) + } else { + json!({ "jsonrpc": "2.0", "id": id, "error": { "code": -32600, "message": format!("the call is past what this server reads ({} bytes and {} values of a job, {LINE_BYTES} bytes of a line)", crate::JOB_BOUND.bytes, crate::JOB_BOUND.values) } }) + }) +} + +/// The tool that verifies a job. +const VERIFY_TOOL: &str = "verify_quantum_result"; + +/// Serve until standard input closes. A line that does not read (not JSON, not UTF-8) is +/// answered -32700 and the session goes on. pub fn serve(memory_gb: u64, key: &SigningKey) { let stdin = std::io::stdin(); + let mut input = stdin.lock(); let mut client = Client { replies: Replies::to(std::io::stdout()), ..Client::default() }; - for line in stdin.lock().lines() { - let Ok(line) = line else { break }; - if line.trim().is_empty() { + while let Ok(Some((line, cut))) = read_line_at_most(&mut input, LINE_BYTES) { + if !cut && line.iter().all(u8::is_ascii_whitespace) { continue; } - let reply = match serde_json::from_str::(&line) { - Ok(msg) => handle(&msg, memory_gb, key, &mut client), - Err(e) => Some(json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })), + let reply = if cut { + // Read as far as it says what it is: a verify call gets its verdict, too_large. + let (id, method, name) = job::message_head(&line); + let msg = json!({ "id": id, "method": method, "params": { "name": name } }); + if id.is_some() { judged(&msg, job::too_large(crate::JOB_BOUND), client.views) } else { Some(json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": format!("a line past the {LINE_BYTES} bytes this server reads, with no id before it") } })) } + } else { + // Read so that a job gets its verdict whatever it is: past what verify reads it is + // too_large, and nested past what a reader takes unreadable. + match job::read_carrying(&line, &[&["params", "arguments", "job"]], Some(crate::JOB_BOUND)) { + Ok((msg, None)) => handle(&msg, memory_gb, key, &mut client), + Ok((msg, Some(verdict))) => judged(&msg, verdict, client.views), + Err(e) => Some(json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })), + } }; if let Some(r) = reply && !client.replies.send(&r) @@ -1203,6 +1254,59 @@ mod tests { assert!(run["result"]["content"][1]["text"].as_str().unwrap().starts_with("2 qubits, 2 gates"), "{run}"); } + /// The tool that verifies a job judges a wasmQ job as `wai-quantum verify` does: its + /// body, its form and its seal (extensions/quantum-wasmq.md §6.5). + #[cfg(feature = "wasmq")] + #[test] + fn an_agent_verifies_a_wasmq_job_as_the_command_line_does() { + use wai_wasmq::envelope::{Bundle, Callout, Module, Run}; + let tool = |name: &str, args: Value| handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": name, "arguments": args } }), 1, &key(), &mut Client::default()).unwrap()["result"].clone(); + // (module (func (export "f") (param i64) (result i64) loop br 0 end i64.const 0)) + let spin = vec![ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x07, 0x05, 0x01, 0x01, b'f', 0x00, 0x00, 0x0a, 0x0b, 0x01, + 0x09, 0x00, 0x03, 0x40, 0x0c, 0x00, 0x0b, 0x42, 0x00, 0x0b, + ]; + let mut run = Run::new(4, 1); + run.per_call = 500; + let b = Bundle { + program: "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[1] q;\nbit[1] d;\nd = f(d);\n".into(), + inputs: Default::default(), + modules: vec![Module { name: "m".into(), bytes: spin, priors: vec![] }], + callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], + run, + }; + let o = wai_wasmq::open(&wai_wasmq::pack(&b).unwrap()).unwrap(); + let (job, _) = wai_wasmq::run(&o, wai_wasmq::run::EngineKind::Interpreter, &wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[7; 32]), false).unwrap(); + assert_eq!(job["result"]["status"], "fault"); + assert_eq!(tool("verify_quantum_result", json!({ "job": job }))["structuredContent"]["valid"], true, "the positive control"); + // Its result sealed again under another key with a contract of a method this version + // does not have: the seal holds, and the job is not one of the profile's. + let mut result = job["result"].clone(); + result.as_object_mut().unwrap().remove("receipt"); + let mut request = job["request"].clone(); + request["wasmq"]["run"]["method"] = json!("statevector"); + result["method"] = json!("statevector"); + let seal = wai_quantum::engine::receipt::seal(&wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[9; 32]), &request, &result); + result["receipt"] = seal; + let resealed = json!({ "status": "fault", "method": "statevector", "request": request, "result": result }); + let v = tool("verify_quantum_result", json!({ "job": resealed })); + assert_eq!(v["structuredContent"]["valid"], false, "{v}"); + // The seal holds: the one reason is the profile's form, and its problem is named. + assert_eq!(v["structuredContent"]["reasons"], json!(["wasmq_form"]), "{v}"); + assert!(v["structuredContent"]["wasmq"]["problems"][0].as_str().unwrap().contains("method"), "{v}"); + // The run that faulted, its body edited to say it was done: judged by what is sealed, + // as the command line judges it, with the host's word noted. + let mut edited = job.clone(); + edited["status"] = json!("done"); + let v = tool("verify_quantum_result", json!({ "job": edited })); + assert_eq!(v["structuredContent"]["valid"], true, "{v}"); + assert_eq!(v["structuredContent"]["wasmq"]["notes"][0], r#"the host's unsealed status says "done"; the sealed result says "fault""#, "{v}"); + assert_eq!(v["structuredContent"]["wasmq"]["outcome"], "fault", "{v}"); + // Given as text, read as every job's text is: the same verdict. + let t = tool("verify_quantum_result", json!({ "job": edited.to_string() })); + assert_eq!(t["structuredContent"]["wasmq"], v["structuredContent"]["wasmq"], "{t}"); + } + #[test] fn an_agent_compiles_a_program_and_the_certificate_verifies() { let tool = |name: &str, args: Value| handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": name, "arguments": args } }), 1, &key(), &mut Client::default()).unwrap()["result"].clone(); @@ -1602,7 +1706,9 @@ mod tests { call_tool("draw_circuit", json!({ "qasm": bell, "format": "svg" }), false, &mut called, &mut hold); call_tool("draw_circuit", json!({ "qasm": bell, "format": "latex" }), false, &mut called, &mut hold); call_tool("draw_circuit", json!({ "qasm": bell, "format": "description" }), false, &mut called, &mut hold); - call_tool("verify_quantum_result", json!({ "job": { "request": {}, "result": {} } }), true, &mut called, &mut hold); + // A result with no receipt is a verdict, that reason alone, as on every surface. + let v = call_tool("verify_quantum_result", json!({ "job": { "request": {}, "result": {} } }), false, &mut called, &mut hold); + assert_eq!(v["reasons"], json!(["no_receipt"]), "{v}"); call_tool("lint_qasm", json!({ "qasm": "OPENQASM 3.0;\ninclude \"mine.inc\";\nqubit q;\nh q;\n" }), false, &mut called, &mut hold); call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.1;\nqubit[2] q;\noutput bit[2] c;\noutput int[32] n = 3;\nx q[1];\nc = measure q;\n", "shots": 20, "compile": { "basis": "standard" } }), false, &mut called, &mut hold); call_tool("draw_circuit", json!({ "qasm": draft }), false, &mut called, &mut hold); diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs new file mode 100644 index 000000000..d2c9ec755 --- /dev/null +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -0,0 +1,387 @@ +//! wasmQ on the command line (extensions/quantum-wasmq.md): `pack-wasmq` puts an +//! OpenQASM 3 program and the WebAssembly modules its `extern`s call into one envelope; +//! `run` runs an envelope, each callout metered, and seals the job receipt; `verify` +//! checks that receipt, and with `--bundle` runs the envelope again and compares; +//! `check-wasmq` holds a module to the profile. + +use clap::Args; +use serde_json::{Value, json}; +use std::path::{Path, PathBuf}; +use wai_wasmq::envelope::{Bundle, Callout, Module, Run}; +use wai_wasmq::run::EngineKind; + +fn die(msg: &str) -> ! { + eprintln!("wai-quantum: {msg}"); + std::process::exit(1) +} + +fn read(p: &Path) -> Vec { + std::fs::read(p).unwrap_or_else(|e| die(&format!("{}: {e}", p.display()))) +} + +/// A file the profile limits: refused for its length, before it is read whole. +fn read_limited(p: &Path, limit: usize, code: &str, what: &str) -> Vec { + if let Ok(m) = std::fs::metadata(p) + && m.len() > limit as u64 + { + die(&format!("[{code}] {}: {} bytes; {what} holds at most {limit}", p.display(), m.len())); + } + read(p) +} + +fn read_envelope(p: &Path) -> Vec { + read_limited(p, wai_wasmq::envelope::MAX_ENVELOPE_BYTES, "W013", "an envelope") +} + +fn read_module(p: &Path) -> Vec { + read_limited(p, wai_wasmq::profile::MAX_MODULE_BYTES, "W010", "a module") +} + +/// Why a bundle goes to no provider. +#[cfg(feature = "providers")] +pub const NO_PROVIDER: &str = "a wasmQ bundle calls its modules between gates, and the provider protocol (wai.quantum.provider/1) carries a fixed program measured at the end, so a bundle runs here, its quantum part simulated. Nothing here has run a module on a provider's machine (extensions/quantum-wasmq.md §7)"; + +/// Whether `file` is an envelope rather than a program. +pub fn is_bundle(file: Option<&Path>) -> bool { + file.and_then(|p| std::fs::File::open(p).ok()) + .map(|mut f| { + let mut m = [0u8; 4]; + std::io::Read::read_exact(&mut f, &mut m).is_ok() && &m == b"WAI1" + }) + .unwrap_or(false) +} + +#[derive(Args, Clone)] +pub struct PackArgs { + /// The OpenQASM 3 program: each `extern` it calls is a callout + pub program: PathBuf, + /// A callout: the `extern`'s name, the module that defines it, and the export if it is not the same name (repeatable) + #[arg(long = "callout", value_name = "NAME=MODULE.wasm[#EXPORT]", required = true)] + pub callouts: Vec, + /// A prior a module takes, in the order given: the module's file and the prior's (repeatable) + #[arg(long = "prior", value_name = "MODULE.wasm=FILE")] + pub priors: Vec, + /// Shots to run + #[arg(long, default_value_t = 1024)] + pub shots: u64, + /// The seed the shots' measurements and the modules' seeds come from + #[arg(long, default_value_t = 1)] + pub seed: u64, + /// `shot`: a fresh instance of each module every shot; `job`: one for the run, shots in order + #[arg(long, default_value = "shot")] + pub state: String, + /// The most fuel one callout may use + #[arg(long, default_value_t = 10_000_000)] + pub fuel_per_call: u64, + /// The most fuel the whole run may use: within what a run may be given, which its host's work bounds (extensions/quantum-wasmq.md §4.8) + #[arg(long, default_value_t = 1 << 34)] + pub fuel_per_job: u64, + /// The most fuel an instance's seed, priors and init may use + #[arg(long, default_value_t = 100_000_000)] + pub fuel_per_instance: u64, + /// Where to write the envelope + #[arg(short, long, value_name = "FILE")] + pub out: PathBuf, +} + +// Its own group's name: `run` flattens the provider flags' `RunArgs` beside it, and two +// groups of one name are refused when the command is built. +#[derive(Args, Clone)] +#[group(id = "wasmq_run")] +pub struct RunArgs { + /// For a wasmQ envelope: the engine, `native` (the default where this build has it) or `interpreter`; the receipt is the same + #[arg(long, value_name = "ENGINE")] + pub engine: Option, + /// For a wasmQ envelope: write every callout's inputs, outputs and fuel here, one JSON object a line + #[arg(long, value_name = "FILE")] + pub transcript: Option, + /// For a wasmQ envelope: run only its modules, on the calls a transcript records (from a run here, or from a machine that ran them), and check each call's output and fuel + #[arg(long, value_name = "FILE", conflicts_with = "transcript")] + pub replay: Option, +} + +impl RunArgs { + /// The first of these flags given: each is for an envelope only. + pub fn given(&self) -> Option<&'static str> { + [(self.engine.is_some(), "--engine"), (self.transcript.is_some(), "--transcript"), (self.replay.is_some(), "--replay")].into_iter().find_map(|(given, flag)| given.then_some(flag)) + } +} + +fn module_name(p: &Path) -> String { + p.file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string() +} + +/// `pack-wasmq`: `inputs` are the values of the program's integer `input`s, as the +/// command line's `--input NAME=VALUE` gives them for every command. +pub fn pack(a: &PackArgs, inputs: wai_quantum::qasm::Inputs) -> ! { + let program = String::from_utf8(read_limited(&a.program, wai_wasmq::envelope::MAX_PROGRAM_BYTES, "W016", "a program")).unwrap_or_else(|_| die("the program is not UTF-8")); + let mut modules: Vec<(PathBuf, Module)> = Vec::new(); + let mut callouts = Vec::new(); + let index_of = |path: PathBuf, modules: &mut Vec<(PathBuf, Module)>| -> usize { + if let Some(i) = modules.iter().position(|(p, _)| *p == path) { + return i; + } + let bytes = read_module(&path); + modules.push((path.clone(), Module { name: module_name(&path), bytes, priors: vec![] })); + modules.len() - 1 + }; + for c in &a.callouts { + let (name, rest) = c.split_once('=').unwrap_or_else(|| die(&format!("--callout {c}: give NAME=MODULE.wasm"))); + let (file, export) = rest.split_once('#').map(|(f, e)| (f, e.to_string())).unwrap_or((rest, name.to_string())); + let module = index_of(PathBuf::from(file), &mut modules); + callouts.push(Callout { name: name.to_string(), module, export }); + } + for p in &a.priors { + let (file, prior) = p.split_once('=').unwrap_or_else(|| die(&format!("--prior {p}: give MODULE.wasm=FILE"))); + let module = index_of(PathBuf::from(file), &mut modules); + let bytes = read_envelope(Path::new(prior)); + modules[module].1.priors.push((module_name(Path::new(prior)), bytes)); + } + let run = Run { shots: a.shots, seed: a.seed, method: String::new(), state: a.state.clone(), per_call: a.fuel_per_call, per_job: a.fuel_per_job, per_init: a.fuel_per_instance }; + let (h, method) = wai_wasmq::envelope::read(&program, &inputs).unwrap_or_else(|r| die(&r.to_string())); + // A contract lists its callouts in the order the program declares its `extern`s, + // whatever order they were given in here. + callouts.sort_by_key(|c| h.externs.iter().position(|d| d.name == c.name).unwrap_or(usize::MAX)); + let bundle = Bundle { program, inputs, modules: modules.into_iter().map(|(_, m)| m).collect(), callouts, run }; + // Nothing is written unless the envelope reads back as this bundle (`pack` holds it to that). + let env = wai_wasmq::pack(&bundle).unwrap_or_else(|r| die(&r.to_string())); + let o = wai_wasmq::open(&env).unwrap_or_else(|r| die(&r.to_string())); + std::fs::write(&a.out, &env).unwrap_or_else(|e| die(&format!("{}: {e}", a.out.display()))); + println!("envelope {} ({} bytes, blake3 {})", a.out.display(), env.len(), o.envelope_blake3); + println!("program {} qubits, {} callouts declared, method {method}", h.reading.program.n_qubits, h.externs.len()); + for m in o.contract["modules"].as_array().into_iter().flatten() { + println!("module {} ({} bytes, blake3 {}), {} priors", m["name"].as_str().unwrap_or(""), m["bytes"], m["blake3"].as_str().unwrap_or(""), m["priors"].as_array().map_or(0, Vec::len)); + } + for c in &bundle.callouts { + println!("callout {} -> {}#{}", c.name, bundle.modules[c.module].name, c.export); + } + std::process::exit(0) +} + +/// A line of a transcript (`run --transcript`): the record of one call, or a refusal. +/// A line missing a member is not a record of shot 0 and no fuel. +fn record_of(line: usize, l: &str) -> wai_wasmq::run::Call { + let bad = |why: &str| -> String { format!("the transcript, line {line}: {why}") }; + let v: Value = serde_json::from_str(l).unwrap_or_else(|e| die(&bad(&e.to_string()))); + const MEMBERS: [&str; 5] = ["shot", "callout", "input", "output", "fuel"]; + if !v.as_object().is_some_and(|o| o.len() == MEMBERS.len() && MEMBERS.iter().all(|m| o.contains_key(*m))) { + die(&bad("a record has exactly shot, callout, input, output and fuel")); + } + let whole = |m: &str| v[m].as_u64().unwrap_or_else(|| die(&bad(&format!("its {m} is not a whole number")))); + let string = |m: &str| v[m].as_str().unwrap_or_else(|| die(&bad(&format!("its {m} is not a string")))); + let bytes = |m: &str| { + let s = string(m).as_bytes(); + if !s.len().is_multiple_of(2) || !s.iter().all(u8::is_ascii_hexdigit) { + die(&bad(&format!("its {m} is not pairs of hexadecimal digits"))); + } + let digit = |c: u8| (c as char).to_digit(16).unwrap_or(0) as u8; + s.chunks(2).map(|p| digit(p[0]) << 4 | digit(p[1])).collect::>() + }; + wai_wasmq::run::Call { shot: whole("shot"), callout: string("callout").to_string(), input: bytes("input"), output: bytes("output"), fuel: whole("fuel") } +} + +pub fn run(file: &Path, json_out: bool, top: usize, key: &wai_quantum::ed25519_dalek::SigningKey, a: &RunArgs) -> ! { + let o = wai_wasmq::open(&read_envelope(file)).unwrap_or_else(|r| die(&r.to_string())); + let kind = match &a.engine { + Some(e) => EngineKind::parse(e).unwrap_or_else(|e| die(&e)), + None => EngineKind::default_here(), + }; + if let Some(path) = &a.replay { + // Read a line at a time: a record of a long run is never held whole. + // A line holds one call's bits twice over in hexadecimal, so it is at most a few + // times the bits a call takes and gives: a longer one is no record, and is not read whole. + const LINE: u64 = (wai_wasmq::envelope::MAX_CALLOUT_BITS / 2) + (1 << 12); + let file = std::fs::File::open(path).unwrap_or_else(|e| die(&format!("{}: {e}", path.display()))); + let mut reader = std::io::BufReader::new(file); + let mut line = 0usize; + let mut lines = std::iter::from_fn(move || { + loop { + let mut buf = Vec::new(); + let n = std::io::BufRead::read_until(&mut std::io::Read::take(&mut reader, LINE + 1), b'\n', &mut buf).unwrap_or_else(|e| die(&format!("the transcript, line {}: {e}", line + 1))); + line += 1; + if n == 0 { + return None; + } + if buf.len() as u64 > LINE { + die(&format!("the transcript, line {line}: longer than the {LINE} bytes a record of a call takes")); + } + let l = String::from_utf8(buf).unwrap_or_else(|_| die(&format!("the transcript, line {line}: not UTF-8"))); + if !l.trim().is_empty() { + return Some(record_of(line, l.trim_end_matches(['\n', '\r']))); + } + } + }); + let r = wai_wasmq::run::replay_from(&o, &mut lines, kind).unwrap_or_else(|e| die(&e.to_string())); + let ok = r.problems.is_empty(); + if json_out { + println!("{}", json!({ "calls": r.calls, "problems": r.problems, "more_problems": r.more_problems, "transcript_blake3": r.transcript_blake3 })); + } else { + println!("replayed {} calls through the envelope's modules", r.calls); + if ok { + println!("result each call's output and fuel as the transcript records"); + } + // What shows that a record is the whole of a run's: each call here replays, and + // this digest is the one its receipt seals, or it is not that run's record. + if let Some(d) = &r.transcript_blake3 { + println!("transcript blake3 {d}, which a receipt of the run that made this record seals, if it ended ok"); + } + for p in &r.problems { + println!("differs {p}"); + } + if r.more_problems > 0 { + println!("differs and {} calls more", r.more_problems); + } + } + std::process::exit(if ok { 0 } else { 1 }) + } + // Each call written as it returns: a record of a long run is never held whole. + let body = match &a.transcript { + Some(path) => { + let file = std::fs::File::create(path).unwrap_or_else(|e| die(&format!("{}: {e}", path.display()))); + let mut w = std::io::BufWriter::new(file); + let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + let mut failed = None; + let mut write = |c: &wai_wasmq::run::Call| { + if failed.is_none() + && let Err(e) = std::io::Write::write_all(&mut w, (json!({ "shot": c.shot, "callout": c.callout, "input": hex(&c.input), "output": hex(&c.output), "fuel": c.fuel }).to_string() + "\n").as_bytes()) + { + failed = Some(e.to_string()); + } + }; + let body = wai_wasmq::run::run_recording(&o, kind, key, Some(&mut write)).unwrap_or_else(|e| die(&e.to_string())); + if let Some(e) = failed.or_else(|| std::io::Write::flush(&mut w).err().map(|e| e.to_string())) { + die(&format!("{}: {e}", path.display())); + } + body + } + None => wai_wasmq::run::run_recording(&o, kind, key, None).unwrap_or_else(|e| die(&e.to_string())), + }; + if json_out { + println!("{body}"); + } else { + print_job(&body, top, kind); + } + std::process::exit(if body["result"]["status"] == "ok" { 0 } else { 1 }) +} + +fn print_job(body: &Value, top: usize, kind: EngineKind) { + let r = &body["result"]; + println!("status {}", r["status"].as_str().unwrap_or("")); + println!("method {} on {} qubits, {} of {} shots, seed {}", r["method"].as_str().unwrap_or(""), r["qubits"], r["shots_run"], r["shots"], r["seed"]); + println!("engine {kind:?} (the receipt is the same on every engine)"); + println!("callouts {} calls, {} fuel ({} of it in instances' seeds, priors and init)", r["callouts"], r["fuel"]["used"], r["fuel"]["instances"]); + for (name, f) in r["fuel"]["callouts"].as_object().into_iter().flatten() { + println!(" {name}: {} calls, {} fuel, at most {} in one call (the budget is {})", f["calls"], f["fuel"], f["max"], r["fuel"]["budget"]["per_call"]); + } + for m in r["modules"].as_array().into_iter().flatten() { + println!("module {} blake3 {}", m["name"].as_str().unwrap_or(""), m["blake3"].as_str().unwrap_or("")); + } + println!("transcript blake3 {}", r["transcript_blake3"].as_str().unwrap_or("")); + if let Some(f) = r.get("fault") { + println!("fault {} in shot {} ({}{}), after {} fuel", f["reason"].as_str().unwrap_or(""), f["shot"], f["phase"].as_str().unwrap_or(""), f.get("callout").and_then(Value::as_str).map(|c| format!(" of {c}")).unwrap_or_default(), f["fuel"]); + } + let mut counts: Vec<(&String, u64)> = r["counts"].as_object().into_iter().flatten().map(|(k, v)| (k, v.as_u64().unwrap_or(0))).collect(); + counts.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0))); + for (k, n) in counts.iter().take(top) { + println!("{k} {n}"); + } + if counts.len() > top { + println!("… {} more outcomes (--top N, or --json)", counts.len() - top); + } + println!("receipt digest {}, signed by {}", r["receipt"]["digest"].as_str().unwrap_or(""), r["receipt"]["key"].as_str().unwrap_or("")); +} + +/// What `verify` prints of a wasmQ job past the receipt's check (`job::check`, which holds +/// it to the profile's form, §6.5): how the run ended by what is sealed, each rule it +/// breaks, the host's unsealed words where they differ, and, with the envelope, whether it +/// is the one the receipt binds and runs again to the same result. Whether the bundle +/// holds: the rest is the check's. +pub fn report(c: &wai_quantum::engine::job::Check, v: &Value, bundle: Option<&Path>) -> bool { + // A job refused before its form is read (unreadable, no receipt) is reported alone + // (the provider protocol's section 8.4): nothing is said of a form that was not read, + // and the envelope is not run again for a reading the verdict refused. + let Some(w) = c.wasmq.clone() else { + crate::not_judged(c.reasons.first().copied().unwrap_or(""), bundle.is_some()); + return true; + }; + let r = &v["result"]; + match (w["outcome"].as_str(), r["fault"]["reason"].as_str()) { + (Some("fault"), Some(reason)) => println!("outcome fault: {reason} in shot {}, after {} of {} shots", r["fault"]["shot"], r["shots_run"], r["shots"]), + (Some(o), _) => println!("outcome {o}: {} of {} shots", r["shots_run"], r["shots"]), + (None, _) => println!("outcome not stated"), + } + let lines = |k: &str| w[k].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect::>(); + let problems = lines("problems"); + if problems.is_empty() { + println!("result a job the profile writes: its sealed contract and result of their forms, fuel within its budgets, a fault it names"); + } + for p in problems { + println!("result NOT a job the profile writes: {p}"); + } + // The host's unsealed words, beside the verdict and no part of it. + for n in lines("notes") { + println!("note {n}"); + } + let Some(b) = bundle else { + println!("bundle not checked (--bundle FILE runs it again and compares)"); + return true; + }; + let (same, reproduced) = rerun(v, b); + println!("bundle {}", if same { "the envelope the receipt binds" } else { "NOT the envelope the receipt binds" }); + match reproduced { + Some(true) => println!("reproduced the same result, byte for byte, run again here"), + Some(false) => println!("reproduced NO: run again here, the result differs"), + None => {} + } + same && reproduced == Some(true) +} + +/// The bundle's part of `verify --json`: `bundle` and `reproduced` beside the job's +/// `wasmq`, and the verdict valid only when both hold. +pub fn bundle_into(v: &Value, bundle: &Path, said: &mut Value) { + let (same, reproduced) = rerun(v, bundle); + said["wasmq"]["bundle"] = json!(same); + said["wasmq"]["reproduced"] = json!(reproduced); + if !(same && reproduced == Some(true)) { + said["valid"] = json!(false); + } +} + +/// Whether `bundle` is the envelope the job's receipt binds, and, when it is, whether +/// running it again here gives the same result, byte for byte, the receipt aside. +fn rerun(v: &Value, bundle: &Path) -> (bool, Option) { + let opened = wai_wasmq::open(&read_envelope(bundle)).unwrap_or_else(|r| die(&r.to_string())); + let verdict = wai_wasmq::verify(v, Some(&opened), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); + (verdict.bundle == Some(true), verdict.reproduced) +} + +pub fn check(module: &Path, json_out: bool) -> ! { + let bytes = read_module(module); + // The profile, and that the module prepares for an engine that needs it prepared. + match wai_wasmq::profile::check(&bytes).and_then(|f| wai_wasmq::meter::prepare(&bytes).map(|_| f)) { + Ok(f) => { + let exports: Vec = f.functions.iter().map(|(n, (p, r))| json!({ "name": n, "params": format!("{p:?}"), "results": format!("{r:?}") })).collect(); + let heaviest = f.weights.iter().max().copied().unwrap_or(0); + let v = json!({ "profile": wai_wasmq::profile::PROFILE, "ok": true, "blake3": wai_wasmq::envelope::blake3_hex(&bytes), "memory_pages": f.memory, "memory_exported": f.memory_exported, "floats": f.floats, "functions": f.weights.len(), "heaviest_frame": heaviest, "exports": exports }); + if json_out { + println!("{v}"); + } else { + println!("profile {} holds", wai_wasmq::profile::PROFILE); + println!("module blake3 {}, {} functions, memory {:?} pages{}, {}", v["blake3"].as_str().unwrap_or(""), f.weights.len(), f.memory, if f.memory_exported { " exported as `memory`" } else { ", not exported as `memory`: no list callout and no prior" }, if f.floats { "floating point (canonical NaNs)" } else { "integers only" }); + println!("stack the heaviest frame weighs {heaviest} of the {} a call may suspend", wai_wasmq::profile::MAX_STACK); + for (n, (p, r)) in &f.functions { + println!("export {n}: {p:?} -> {r:?}"); + } + } + std::process::exit(0) + } + Err(r) => { + if json_out { + println!("{}", json!({ "profile": wai_wasmq::profile::PROFILE, "ok": false, "code": r.code, "message": r.message })); + } else { + eprintln!("{r}"); + } + std::process::exit(1) + } + } +} diff --git a/wai/wai-quantum-cli/tests/verify.rs b/wai/wai-quantum-cli/tests/verify.rs index f393ce2ea..96ed49d0d 100644 --- a/wai/wai-quantum-cli/tests/verify.rs +++ b/wai/wai-quantum-cli/tests/verify.rs @@ -110,8 +110,9 @@ fn the_mcp_tool_gives_the_corpus_s_verdict_on_what_a_parsed_job_shows() { for e in manifest["entries"].as_array().unwrap() { let name = e["file"].as_str().unwrap(); let want: Vec<&str> = e["reasons"].as_array().unwrap().iter().filter_map(Value::as_str).collect(); - // Nesting as deep as a reader allows, which a message around it takes past that. - if want.contains(&"unreadable") || want.contains(&"not_a_job") || want.contains(&"no_receipt") || name.contains("depth") { + // What only a text shows is the text's; a job as deep as a reader allows is read + // with room for the message around it (job::read_message), and gets its verdict. + if want.contains(&"unreadable") { continue; } let Ok(job) = serde_json::from_slice::(&std::fs::read(corpus().join(name)).unwrap()) else { continue }; @@ -179,3 +180,296 @@ fn the_mcp_tool_given_a_job_s_text_gives_the_corpus_s_verdict() { assert_eq!(got, want, "{name}"); } } + +/// A wasmQ job is a job like any other: `verify --json`, `verify`, `--lines` and the MCP +/// tool, given its text and given it parsed, each give every wasmQ job of the corpus the +/// reasons its manifest writes by hand (the provider protocol's §8.4, quantum-wasmq §6.5). +#[test] +fn a_wasmq_job_gets_the_corpus_s_verdict_from_every_reader() { + let manifest: Value = serde_json::from_str(&std::fs::read_to_string(corpus().join("manifest.json")).unwrap()).unwrap(); + let sorted = |v: &Value| -> Vec { + let mut r: Vec = v.as_array().unwrap_or_else(|| panic!("no reasons: {v}")).iter().filter_map(Value::as_str).map(str::to_string).collect(); + r.sort(); + r + }; + let entries: Vec<&Value> = manifest["entries"].as_array().unwrap().iter().filter(|e| e["file"].as_str().unwrap().contains("wasmq")).collect(); + assert!(entries.len() >= 12, "{}", entries.len()); + let dir = scratch("wasmq-every-reader"); + let (mut lines, mut wants) = (String::new(), Vec::new()); + for e in entries { + let name = e["file"].as_str().unwrap(); + let want = sorted(&e["reasons"]); + let path = corpus().join(name); + let path = path.to_str().unwrap(); + let out = verify(&["--json", path]); + let said: Value = serde_json::from_slice(&out.stdout).unwrap_or_else(|err| panic!("{name}: --json gives no JSON verdict ({err}): {}", String::from_utf8_lossy(&out.stdout))); + assert_eq!((sorted(&said["reasons"]), out.status.success()), (want.clone(), want.is_empty()), "{name}: verify --json"); + let out = verify(&[path]); + assert_eq!(out.status.success(), want.is_empty(), "{name}: verify\n{}", String::from_utf8_lossy(&out.stdout)); + let text = std::fs::read_to_string(path).unwrap(); + assert_eq!(sorted(&mcp("verify_quantum_result", json!({ "job": text }))["reasons"]), want, "{name}: the tool, given the text"); + let parsed: Value = serde_json::from_str(&text).unwrap(); + assert_eq!(sorted(&mcp("verify_quantum_result", json!({ "job": parsed }))["reasons"]), want, "{name}: the tool, given the job"); + lines.push_str(&format!("{parsed}\n")); + wants.push((name.to_string(), want)); + } + let file = dir.join("wasmq.jsonl"); + std::fs::write(&file, lines).unwrap(); + let out = verify(&["--lines", file.to_str().unwrap()]); + let said: Vec = String::from_utf8_lossy(&out.stdout).lines().map(|l| serde_json::from_str(l).unwrap()).collect(); + assert_eq!(said.len(), wants.len()); + for ((name, want), v) in wants.iter().zip(&said) { + assert_eq!(&sorted(&v["reasons"]), want, "{name}: --lines"); + } + assert!(!out.status.success(), "some are not valid"); + let _ = std::fs::remove_dir_all(&dir); +} + + +/// `--lines` reads each line on its own, as the second verifier does: a line that is not +/// UTF-8, or blank, is that line's `unreadable`, and the lines around it keep their verdicts; +/// the newline that ends the file ends its last line; a file of no line is refused. +#[test] +fn lines_read_each_line_on_its_own() { + let dir = scratch("lines-each"); + let job = |name: &str| -> String { serde_json::from_slice::(&std::fs::read(corpus().join("jobs").join(name)).unwrap()).unwrap().to_string() }; + let (run, fault) = (job("cli-wasmq-run.json"), job("cli-wasmq-fault.json")); + let mut bytes = Vec::new(); + for line in [run.as_bytes(), b"", b" ", &[b'{', 0xff, b'}'], fault.as_bytes()] { + bytes.extend_from_slice(line); + bytes.push(b'\n'); + } + let file = dir.join("mixed.jsonl"); + std::fs::write(&file, &bytes).unwrap(); + let out = verify(&["--lines", file.to_str().unwrap()]); + let said: Vec = String::from_utf8_lossy(&out.stdout).lines().map(|l| serde_json::from_str(l).unwrap()).collect(); + let reasons: Vec = said.iter().map(|v| v["reasons"].clone()).collect(); + assert_eq!(reasons, vec![json!([]), json!(["unreadable"]), json!(["unreadable"]), json!(["unreadable"]), json!([])], "{said:?}"); + assert!(!out.status.success()); + // The last line with no newline after it is a line. + std::fs::write(&file, run.as_bytes()).unwrap(); + let out = verify(&["--lines", file.to_str().unwrap()]); + assert!(out.status.success() && String::from_utf8_lossy(&out.stdout).lines().count() == 1, "{}", String::from_utf8_lossy(&out.stdout)); + std::fs::write(&file, b"").unwrap(); + let out = verify(&["--lines", file.to_str().unwrap()]); + assert!(!out.status.success() && out.stdout.is_empty() && String::from_utf8_lossy(&out.stderr).contains("no job"), "{}", String::from_utf8_lossy(&out.stderr)); + let _ = std::fs::remove_dir_all(&dir); +} + +/// A wasmQ job is judged as one whatever else its result says it is: one that also names +/// the algorithms' schema goes to no other verifier, and breaks its profile's form. +#[test] +fn a_wasmq_job_naming_another_schema_is_judged_as_a_wasmq_job() { + let dir = scratch("wasmq-schema"); + let mut body: Value = serde_json::from_slice(&std::fs::read(corpus().join("jobs/cli-wasmq-run.json")).unwrap()).unwrap(); + body["result"]["schema"] = json!(wai_quantum::engine::schema::ALGORITHMS); + let mut result = body["result"].clone(); + result.as_object_mut().unwrap().remove("receipt"); + body["result"]["receipt"] = wai_quantum::engine::receipt::seal(&key(), &body["request"], &result); + let file = dir.join("schema.json"); + std::fs::write(&file, body.to_string()).unwrap(); + let out = verify(&["--json", file.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&out.stdout).unwrap_or_else(|_| panic!("{}", String::from_utf8_lossy(&out.stdout))); + assert_eq!(said["reasons"], json!(["wasmq_form"]), "{said}"); + assert!(said["wasmq"]["problems"][0].as_str().unwrap().contains("schema"), "{said}"); + let out = verify(&[file.to_str().unwrap()]); + let text = String::from_utf8_lossy(&out.stdout); + assert!(!out.status.success() && text.contains("signature VALID"), "{text}"); + assert_eq!(mcp("verify_quantum_result", json!({ "job": body.to_string() }))["reasons"], json!(["wasmq_form"])); + assert_eq!(mcp("verify_quantum_result", json!({ "job": body }))["reasons"], json!(["wasmq_form"])); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Every body gets its verdict from `verify`, as JSON and printed: text that does not read +/// and a body that is no job are reported alone and said not to be judged, never "not JSON" +/// or the research tools' receipt reader; an estimate or an algorithm's answer with no +/// receipt, or that is no job, is that reason alone on the command line and from its MCP +/// tool, given its text or the job; and a forged estimate sealed again is run again. +#[test] +fn every_body_gets_its_verdict_from_verify() { + let dir = scratch("every-body"); + let job = std::fs::read_to_string(corpus().join("jobs/cli-wasmq-fault.json")).unwrap(); + let parsed: Value = serde_json::from_str(&job).unwrap(); + let mut deep = String::from("0"); + for _ in 0..128 { + deep = format!("[{deep}]"); + } + let estimate = wai_quantum::engine::estimate::FORMAT; + let algorithms = wai_quantum::engine::schema::ALGORITHMS; + let mut problems = Vec::new(); + for (what, body, want) in [ + ("a byte order mark", format!("\u{feff}{job}"), "unreadable"), + ("a form feed before the job", format!("\u{c}{job}"), "unreadable"), + ("nothing", String::new(), "unreadable"), + ("trailing text", format!("{job} x"), "unreadable"), + ("a lone surrogate", job.replacen("\"method\":\"stabilizer\"", "\"method\":\"\\ud800\"", 1), "unreadable"), + ("128 deep", deep.clone(), "unreadable"), + ("an empty object", "{}".into(), "not_a_job"), + ("null", "null".into(), "not_a_job"), + ("a string", "\"job\"".into(), "not_a_job"), + ("a wasmQ result alone", json!({ "result": parsed["result"] }).to_string(), "not_a_job"), + ("an estimate with no receipt", json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\nh q;\n" }, "result": { "format": estimate } }).to_string(), "no_receipt"), + ("an estimate with no receipt and no program", json!({ "request": {}, "result": { "format": estimate } }).to_string(), "no_receipt"), + ("an estimate that is no job", json!({ "request": 5, "result": { "format": estimate } }).to_string(), "not_a_job"), + ("an algorithm's answer with no receipt", json!({ "request": {}, "result": { "schema": algorithms, "call": "gradient" } }).to_string(), "no_receipt"), + ("an algorithm's answer that is no job", json!({ "request": 5, "result": { "schema": algorithms } }).to_string(), "not_a_job"), + ] { + let file = dir.join("job.json"); + std::fs::write(&file, &body).unwrap(); + let f = file.to_str().unwrap(); + let o = verify(&["--json", f]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or(Value::Null); + if o.status.success() || said["reasons"] != json!([want]) { + problems.push(format!("{what}, --json: {said} {}", String::from_utf8_lossy(&o.stderr))); + } + let o = verify(&[f]); + let out = String::from_utf8_lossy(&o.stdout); + if o.status.success() || !out.contains(&format!("INVALID ({want})")) || !out.contains("not judged") { + problems.push(format!("{what}, printed: {out} {}", String::from_utf8_lossy(&o.stderr))); + } + // The tool takes the job's text, or the job (a string given is its text). + let mut forms = vec![("text", json!(body))]; + if let Ok(v) = serde_json::from_str::(&body) + && !v.is_string() + { + forms.push(("job", v)); + } + for (form, given) in forms { + let said = mcp("verify_quantum_result", json!({ "job": given })); + if said["reasons"] != json!([want]) { + problems.push(format!("{what}, the MCP tool given its {form}: {said}")); + } + } + } + // A wasmQ result alone with --bundle: not judged, and the bundle not run. + let file = dir.join("alone.json"); + std::fs::write(&file, json!({ "result": parsed["result"] }).to_string()).unwrap(); + let env = corpus().join("../wasmq/envelopes/fault-fuel.wai"); + let o = verify(&[file.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + let out = String::from_utf8_lossy(&o.stdout); + if o.status.success() || !out.contains("bundle not run") { + problems.push(format!("a wasmQ result alone, --bundle: {out}")); + } + // A forged estimate, sealed again: the receipt holds, and the run again does not. + let key = key(); + let mut forged = wai_quantum::engine::estimate::sealed(&json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nrz(0.3) q;\n", "budget": 1e-3 }), &key).unwrap(); + forged["result"]["estimate"]["physical_qubits"] = json!(1); + forged["result"].as_object_mut().unwrap().remove("receipt"); + wai_quantum::engine::receipt::seal_into(&key, &forged["request"].clone(), &mut forged["result"]); + let file = dir.join("forged.json"); + std::fs::write(&file, forged.to_string()).unwrap(); + let said: Value = serde_json::from_slice(&verify(&["--json", file.to_str().unwrap()]).stdout).unwrap_or(Value::Null); + let by_text = mcp("verify_quantum_result", json!({ "job": forged.to_string() })); + let by_object = mcp("verify_quantum_result", json!({ "job": forged })); + for (form, v) in [("--json", said), ("the tool, its text", by_text), ("the tool, the job", by_object)] { + if v["valid"] != false || v["reproduced"] != false || v["signed"] != true || v["reasons"] != json!([]) { + problems.push(format!("a forged estimate, {form}: {v}")); + } + } + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + +/// Lines given to the command line's MCP server, raw, and the replies by id. +fn mcp_lines(lines: &[Vec]) -> Vec { + let mut child = Command::new(BIN).arg("mcp").env_remove("WAI_QUANTUM_KEY").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + let all: Vec = lines.iter().flat_map(|l| l.iter().copied().chain(*b"\n")).collect(); + let writer = std::thread::spawn(move || { + let _ = stdin.write_all(&all); + }); + let out = child.wait_with_output().unwrap(); + let _ = writer.join(); + String::from_utf8_lossy(&out.stdout).lines().filter_map(|l| serde_json::from_str::(l).ok()).collect() +} + +/// The command line's MCP server keeps its session through a line that does not read (not +/// UTF-8, a byte of it inside a job's text among them, or not JSON), answering it -32700; +/// gives a job nested past what a reader takes +/// `unreadable`, as an object or as text; and a job past what `verify` reads `too_large`, +/// by its id. +#[test] +fn the_mcp_server_answers_every_line_and_every_job() { + let call = |id: u32, args: String| format!("{{\"jsonrpc\":\"2.0\",\"id\":{id},\"method\":\"tools/call\",\"params\":{{\"name\":\"verify_quantum_result\",\"arguments\":{args}}}}}").into_bytes(); + // One line each: the job's text without its line breaks. + let text = serde_json::from_slice::(&std::fs::read(corpus().join("jobs/cli-run-statevector.json")).unwrap()).unwrap().to_string(); + let open = text.strip_suffix('}').unwrap().to_string(); + let deep = |d: usize| format!("{open}, \"note\": {}{}}}", "[".repeat(d - 1), "]".repeat(d - 1)); + let mut bad = call(3, format!("{{\"job\": {}}}", json!(format!("{open}, \"note\": \"x\"}}")))); + bad.insert(bad.len() - 30, 0xff); + let mut lines = vec![ + br#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}"#.to_vec(), + call(2, format!("{{\"job\": {text}}}")), + bad, + b"{not json".to_vec(), + vec![0xed, 0xa0, 0x80], + ]; + let depths = [127usize, 128, 136, 200, 100_000]; + for (k, d) in depths.iter().enumerate() { + lines.push(call(10 + k as u32, format!("{{\"job\": {}}}", deep(*d)))); + lines.push(call(20 + k as u32, format!("{{\"job\": {}}}", json!(deep(*d))))); + } + // Past the values verify reads, in a short text. + lines.push(call(30, format!("{{\"job\": {open}, \"note\": [{}0]}}}}", "0,".repeat(1 << 22)))); + lines.push(br#"{"jsonrpc":"2.0","id":99,"method":"ping"}"#.to_vec()); + let replies = mcp_lines(&lines); + let by = |id: u32| replies.iter().find(|r| r["id"] == id).cloned().unwrap_or_else(|| panic!("no reply to {id}: {replies:?}")); + let s = |id: u32| by(id)["result"]["structuredContent"].clone(); + assert_eq!(s(2)["valid"], true, "{}", by(2)); + assert!(replies.iter().all(|r| r["id"] != 3), "a line that is no UTF-8 has no id to answer by"); + let errors: Vec<&Value> = replies.iter().filter(|r| r["id"].is_null()).collect(); + assert!(errors.len() == 3 && errors.iter().all(|r| r["error"]["code"] == -32700), "the three lines that do not read: {replies:?}"); + for (k, d) in depths.iter().enumerate() { + for id in [10 + k as u32, 20 + k as u32] { + let want = if *d <= 127 { json!([]) } else { json!(["unreadable"]) }; + assert_eq!(s(id)["reasons"], want, "a job {d} deep, call {id}: {}", by(id)); + } + } + assert_eq!((s(30)["reasons"].clone(), s(30)["read_at_most"]["values"].clone()), (json!(["too_large"]), json!(1 << 22)), "{}", by(30)); + assert!(by(99)["result"].is_object(), "the session goes on after every line"); +} + +/// `verify` gives a verdict at its own bound, in each shape a check holds most of: a job of +/// 384 MiB (a long string beside it), and one of 2^22 values (numbers beside it). One more +/// byte, or one more value, is `too_large`. +#[test] +fn verify_gives_a_verdict_at_its_bound() { + let d = scratch("at-the-bound"); + let job: Value = serde_json::from_slice(&std::fs::read(corpus().join("jobs/cli-run-statevector.json")).unwrap()).unwrap(); + let text = job.to_string(); + let open = &text[..text.len() - 1]; + let check = |name: &str, body: String, valid: bool| { + let path = d.join(name); + std::fs::write(&path, &body).unwrap(); + drop(body); + let o = verify(&["--json", path.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("{name}: no verdict: {}", String::from_utf8_lossy(&o.stderr))); + let _ = std::fs::remove_file(&path); + if valid { + assert_eq!(said["valid"], true, "{name}: {said}"); + } else { + assert_eq!(said["reasons"], json!(["too_large"]), "{name}: {said}"); + } + }; + let long = |n: usize| { + let rest = open.len() + ",\"pad\":\"\"}".len(); + format!("{open},\"pad\":\"{}\"}}", "x".repeat(n - rest)) + }; + check("long.json", long(384 << 20), true); + check("longer.json", long((384 << 20) + 1), false); + // Its values, as §8.4 counts them: one, and one more for each `,` `:` `[` `{` outside strings. + let (mut values, mut in_string, mut escaped) = (1u64, false, false); + for b in text.bytes() { + match b { + _ if in_string && escaped => escaped = false, + b'\\' if in_string => escaped = true, + b'"' => in_string = !in_string, + b',' | b':' | b'[' | b'{' if !in_string => values += 1, + _ => {} + } + } + // The job's values, and `,` `:` `[` and a comma for each `0,` of the pad. + let many = |n: u64| format!("{open},\"pad\":[{}0]}}", "0,".repeat((n - values - 3) as usize)); + check("many.json", many(1 << 22), true); + check("more.json", many((1 << 22) + 1), false); + let _ = std::fs::remove_dir_all(&d); +} diff --git a/wai/wai-quantum-cli/tests/verify_at_bound.py b/wai/wai-quantum-cli/tests/verify_at_bound.py new file mode 100644 index 000000000..b7a2f3017 --- /dev/null +++ b/wai/wai-quantum-cli/tests/verify_at_bound.py @@ -0,0 +1,30 @@ +"""Writes jobs at `wai-quantum verify`'s bound into DIR, for the WASI component to judge as +the native build does: long.json (384 MiB), longer.json (a byte more), many.json (2^22 +values) and more.json (a value more). Standard library only.""" +import os, sys +d = sys.argv[1] +here = os.path.dirname(os.path.abspath(__file__)) +text = open(os.path.join(here, "../../quantum-conformance/receipts/jobs/cli-run-statevector.json"), encoding="utf-8").read().strip() +assert text.endswith("}") +head = text[:-1] +def values(t): + n, ins, esc = 1, False, False + for c in t: + if ins: + if esc: esc = False + elif c == "\\": esc = True + elif c == '"': ins = False + continue + if c == '"': ins = True + elif c in ",:[{": n += 1 + return n +def long(n): + rest = len(head.encode()) + len(',"pad":""}') + return head + ',"pad":"' + "x" * (n - rest) + '"}' +v = values(text) +def many(n): + return head + ',"pad":[' + "0," * (n - v - 3) + '0]}' +for name, body in (("long", long(384 << 20)), ("longer", long((384 << 20) + 1)), ("many", many(1 << 22)), ("more", many((1 << 22) + 1))): + with open(os.path.join(d, name + ".json"), "w", encoding="utf-8") as f: + f.write(body) +print("written", values(many(1 << 22)), len(long(384 << 20).encode())) diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs new file mode 100644 index 000000000..212c0b124 --- /dev/null +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -0,0 +1,467 @@ +//! wasmQ through the binary (extensions/quantum-wasmq.md): a program and the reference +//! decoder packed into an envelope, run with each callout metered, the job receipt checked +//! and the envelope run again by `verify --bundle`; a fault ends in a receipt that still +//! verifies; a module outside the profile is refused with its code; a provider is refused. +#![cfg(feature = "wasmq")] + +use serde_json::Value; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +const BIN: &str = env!("CARGO_BIN_EXE_wai-quantum"); +const DECODER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../quantum-conformance/wasmq/modules/qldpc_decoder.wasm"); +const CLOCK: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../quantum-conformance/wasmq/refuse/import-clock.wasm"); + +fn scratch(name: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("wai-quantum-wasmq-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d +} + +fn run(home: &Path, args: &[&str]) -> Output { + Command::new(BIN).args(args).env("XDG_CONFIG_HOME", home.join("config")).env_remove("WAI_QUANTUM_KEY").output().unwrap() +} + +fn text(o: &Output) -> String { + format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) +} + +/// Writes surface(3)'s memory round and its prior; their paths. +fn surface3(d: &Path) -> (PathBuf, PathBuf) { + let code = wai_quantum::quantum_qec::CssCode::surface(3); + let prog = d.join("memory.qasm"); + std::fs::write(&prog, wai_wasmq::reference::qec_program(code.n, &code.checks, 1)).unwrap(); + let prior = d.join("checks.bin"); + std::fs::write(&prior, code.check_matrix_bytes()).unwrap(); + (prog, prior) +} + +#[test] +fn pack_run_and_verify_a_memory_round_decoded_in_the_middle() { + let d = scratch("round"); + let (prog, prior) = surface3(&d); + let env = d.join("round.wai"); + let callout = format!("decode64={DECODER}"); + let prior_arg = format!("{DECODER}={}", prior.display()); + let o = run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &callout, "--prior", &prior_arg, "--shots", "24", "--seed", "3", "-o", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + assert!(text(&o).contains("callout decode64 -> qldpc_decoder#decode64"), "{}", text(&o)); + + let mut bodies = Vec::new(); + for engine in ["native", "interpreter"] { + let o = run(&d, &["run", env.to_str().unwrap(), "--json", "--demo-key", "--engine", engine]); + assert!(o.status.success(), "{}", text(&o)); + bodies.push(String::from_utf8(o.stdout).unwrap()); + } + assert_eq!(bodies[0], bodies[1], "the same job, byte for byte, on both engines"); + let job: Value = serde_json::from_str(&bodies[0]).unwrap(); + assert_eq!(job["result"]["status"], "ok"); + assert_eq!(job["result"]["callouts"], 24); + let jf = d.join("job.json"); + std::fs::write(&jf, &bodies[0]).unwrap(); + let o = run(&d, &["verify", jf.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + assert!(text(&o).contains("reproduced the same result, byte for byte"), "{}", text(&o)); + + // A changed result does not verify. + let mut forged = job.clone(); + forged["result"]["counts"] = serde_json::json!({ "0": 24 }); + std::fs::write(&jf, forged.to_string()).unwrap(); + let o = run(&d, &["verify", jf.to_str().unwrap()]); + assert!(!o.status.success()); + assert!(text(&o).contains("signature INVALID")); + + // The human form names the fuel and the transcript. + let tr = d.join("calls.jsonl"); + let o = run(&d, &["run", env.to_str().unwrap(), "--demo-key", "--transcript", tr.to_str().unwrap()]); + assert!(text(&o).contains("decode64: 24 calls"), "{}", text(&o)); + assert_eq!(std::fs::read_to_string(&tr).unwrap().lines().count(), 24); + // The transcript replays through the modules alone; a changed output does not. + let o = run(&d, &["run", env.to_str().unwrap(), "--replay", tr.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + let t = std::fs::read_to_string(&tr).unwrap(); + let first: Value = serde_json::from_str(t.lines().next().unwrap()).unwrap(); + let out = first["output"].as_str().unwrap(); + let flipped = format!("{}{}", if &out[..1] == "0" { "1" } else { "0" }, &out[1..]); + std::fs::write(&tr, t.replacen(out, &flipped, 1)).unwrap(); + let o = run(&d, &["run", env.to_str().unwrap(), "--replay", tr.to_str().unwrap()]); + assert_eq!(o.status.code(), Some(1), "{}", text(&o)); + assert!(text(&o).contains("differs"), "{}", text(&o)); +} + +#[test] +fn a_callout_past_its_budget_ends_in_a_fault_receipt_that_verifies() { + let d = scratch("fault"); + let (prog, prior) = surface3(&d); + let env = d.join("tight.wai"); + let o = run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &format!("decode64={DECODER}"), "--prior", &format!("{DECODER}={}", prior.display()), "--shots", "4", "--fuel-per-call", "50", "-o", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + let o = run(&d, &["run", env.to_str().unwrap(), "--json", "--demo-key"]); + assert_eq!(o.status.code(), Some(1), "a fault exits 1"); + let job: Value = serde_json::from_slice(&o.stdout).unwrap(); + assert_eq!(job["status"], "fault"); + assert_eq!(job["result"]["fault"]["reason"], "fuel_exhausted"); + let jf = d.join("job.json"); + std::fs::write(&jf, &o.stdout).unwrap(); + let o = run(&d, &["verify", jf.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); +} + +#[test] +fn a_module_outside_the_profile_is_refused_with_its_code() { + let d = scratch("check"); + let o = run(&d, &["check-wasmq", DECODER]); + assert!(o.status.success(), "{}", text(&o)); + assert!(text(&o).contains("integers only")); + let o = run(&d, &["check-wasmq", CLOCK, "--json"]); + assert_eq!(o.status.code(), Some(1)); + let v: Value = serde_json::from_slice(&o.stdout).unwrap(); + assert_eq!(v["code"], "W002"); + let (prog, _) = surface3(&d); + let o = run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &format!("decode64={CLOCK}#f"), "-o", d.join("x.wai").to_str().unwrap()]); + assert!(!o.status.success()); + assert!(text(&o).contains("[W002]"), "{}", text(&o)); +} + +#[cfg(feature = "providers")] +#[test] +fn a_bundle_goes_to_no_provider() { + let d = scratch("provider"); + let env = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../quantum-conformance/wasmq/envelopes/surface3-decode64.wai")); + let o = run(&d, &["run", env.to_str().unwrap(), "--provider", "anything"]); + assert!(!o.status.success()); + assert!(text(&o).contains("so a bundle runs here"), "{}", text(&o)); + let o = run(&d, &["run", env.to_str().unwrap(), "--shots", "5"]); + assert!(text(&o).contains("states its own shots"), "{}", text(&o)); +} + +/// `(func (export "f") (param i64) (result i64) local.get 0)`, as bytes. +const IDENTITY: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x07, 0x05, 0x01, 0x01, 0x66, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, + 0x20, 0x00, 0x0b, +]; + +#[test] +fn verify_names_the_signer_and_the_outcome_and_notes_an_edit_no_signature_covers() { + let d = scratch("signer"); + let (prog, prior) = surface3(&d); + let env = d.join("round.wai"); + let o = run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &format!("decode64={DECODER}"), "--prior", &format!("{DECODER}={}", prior.display()), "--shots", "8", "-o", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + // The same envelope run under two keys: the receipts differ only in who signed. + let other = d.join("other.key"); + std::fs::write(&other, "11".repeat(32)).unwrap(); + let mut signers = Vec::new(); + for (name, key) in [("demo", vec!["--demo-key"]), ("other", vec!["--key", other.to_str().unwrap()])] { + let mut args = vec!["run", env.to_str().unwrap(), "--json"]; + args.extend(key); + let o = run(&d, &args); + assert!(o.status.success(), "{}", text(&o)); + let jf = d.join(format!("{name}.json")); + std::fs::write(&jf, &o.stdout).unwrap(); + let v = run(&d, &["verify", jf.to_str().unwrap()]); + assert!(v.status.success(), "{}", text(&v)); + let out = String::from_utf8_lossy(&v.stdout).to_string(); + let signer = out.lines().find(|l| l.starts_with("signer ")).unwrap_or_else(|| panic!("verify names no signer:\n{out}")).to_string(); + assert!(signer.contains("did:key:"), "{signer}"); + assert!(out.lines().any(|l| l.starts_with("outcome ") && l.contains("ok")), "verify does not say how the run ended:\n{out}"); + signers.push(signer); + } + assert_ne!(signers[0], signers[1], "two keys, one signer line"); + // The body's status is not under the signature: the verdict is the sealed content's, + // and the host's edit is noted beside it. + let mut job: Value = serde_json::from_slice(&std::fs::read(d.join("demo.json")).unwrap()).unwrap(); + job["status"] = serde_json::json!("fault"); + let jf = d.join("edited.json"); + std::fs::write(&jf, job.to_string()).unwrap(); + let v = run(&d, &["verify", jf.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + let out = text(&v); + assert!(v.status.success(), "the sealed content holds:\n{out}"); + assert!(out.lines().any(|l| l.starts_with("outcome ") && l.contains("ok")), "the outcome is the sealed result's:\n{out}"); + assert!(out.contains(r#"note the host's unsealed status says "fault"; the sealed result says "done""#), "the edit is noted:\n{out}"); +} + +#[test] +fn pack_wasmq_takes_a_generator_s_inputs() { + let d = scratch("inputs"); + let prog = d.join("gen.qasm"); + std::fs::write(&prog, "OPENQASM 3.0;\ninclude \"stdgates.inc\";\ninput uint n;\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nfor uint i in [0:n - 1] { h q[i]; }\nb = measure q;\nr = f(b);\n").unwrap(); + let module = d.join("id.wasm"); + std::fs::write(&module, IDENTITY).unwrap(); + let env = d.join("gen.wai"); + let o = run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &format!("f={}", module.display()), "--input", "n=3", "--shots", "64", "-o", env.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + let o = run(&d, &["run", env.to_str().unwrap(), "--json", "--demo-key"]); + assert!(o.status.success(), "{}", text(&o)); + let job: Value = serde_json::from_slice(&o.stdout).unwrap(); + assert_eq!(job["request"]["wasmq"]["program"]["inputs"]["n"], 3); + // Three qubits were put in superposition: only the low three bits of an outcome vary. + for k in job["result"]["counts"].as_object().unwrap().keys() { + assert_eq!(&k[..5], "00000", "{k}"); + assert_eq!(&k[8..13], "00000", "{k}"); + } + assert!(job["result"]["counts"].as_object().unwrap().len() > 4); +} + +#[test] +fn a_transcript_line_that_is_no_record_is_refused_and_a_replay_states_the_digest_a_receipt_seals() { + let d = scratch("record"); + let (prog, prior) = surface3(&d); + let env = d.join("round.wai"); + let callout = format!("decode64={DECODER}"); + let prior_arg = format!("{DECODER}={}", prior.display()); + assert!(run(&d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &callout, "--prior", &prior_arg, "--shots", "4", "-o", env.to_str().unwrap()]).status.success()); + let tr = d.join("calls.jsonl"); + let o = run(&d, &["run", env.to_str().unwrap(), "--json", "--demo-key", "--transcript", tr.to_str().unwrap()]); + assert!(o.status.success(), "{}", text(&o)); + let job: Value = serde_json::from_slice(&o.stdout).unwrap(); + let digest = job["result"]["transcript_blake3"].as_str().unwrap(); + // A replay states the digest of what it read: the receipt's, for a record of that run. + let replayed = |path: &Path| run(&d, &["run", env.to_str().unwrap(), "--replay", path.to_str().unwrap()]); + let o = replayed(&tr); + assert!(o.status.success(), "{}", text(&o)); + assert!(text(&o).contains(digest), "the replay names the digest the receipt seals: {}", text(&o)); + // A call left out is still a record whose calls replay; its digest is not the receipt's. + let lines: Vec = std::fs::read_to_string(&tr).unwrap().lines().map(str::to_string).collect(); + let short = d.join("short.jsonl"); + std::fs::write(&short, lines[..lines.len() - 1].join("\n") + "\n").unwrap(); + let o = replayed(&short); + assert!(o.status.success(), "{}", text(&o)); + assert!(!text(&o).contains(digest), "a record with a call left out: {}", text(&o)); + // A line that is not a record is refused, not read as a record of shot 0 and no fuel. + let line: Value = serde_json::from_str(&lines[0]).unwrap(); + let mut cases: Vec<(&str, Value)> = Vec::new(); + for member in ["shot", "callout", "input", "output", "fuel"] { + let mut l = line.clone(); + l.as_object_mut().unwrap().remove(member); + cases.push(("a member missing", l)); + } + for (member, value) in [("shot", serde_json::json!("0")), ("shot", serde_json::json!(-1)), ("fuel", serde_json::json!(1.5)), ("input", serde_json::json!(7)), ("output", serde_json::json!(null)), ("callout", serde_json::json!(0))] { + let mut l = line.clone(); + l[member] = value; + cases.push(("a member of another type", l)); + } + let mut l = line.clone(); + l["note"] = serde_json::json!("a member no record has"); + cases.push(("another member", l)); + let bad = d.join("bad.jsonl"); + // A line longer than any record of a call is refused before it is read whole. + std::fs::write(&bad, "x".repeat(5 << 20) + "\n").unwrap(); + let o = replayed(&bad); + assert!(!o.status.success() && text(&o).contains("longer than"), "a line of 5 MiB: {}", text(&o)); + for (what, l) in cases { + std::fs::write(&bad, l.to_string() + "\n").unwrap(); + let o = replayed(&bad); + assert!(!o.status.success(), "{what}: {l} replayed: {}", text(&o)); + assert!(text(&o).contains("the transcript"), "{what}: {l} is refused as a transcript's line, not reported as a call that differs: {}", text(&o)); + } +} + +// On Linux, where a sparse file costs nothing and a host refuses to reserve this much: were a +// regression to read the file whole, a host that overcommits would try to. +#[cfg(target_os = "linux")] +#[test] +fn a_file_past_the_profile_s_limits_is_refused_before_it_is_read() { + let d = scratch("huge"); + let sparse = |name: &str, head: &[u8]| { + let path = d.join(name); + let file = std::fs::File::create(&path).unwrap(); + std::io::Write::write_all(&mut &file, head).unwrap(); + file.set_len(1 << 42).ok().map(|_| path) + }; + let (Some(envelope), Some(module)) = (sparse("huge.wai", b"WAI1"), sparse("huge.wasm", b"\0asm")) else { + return; // this file system holds no file so long + }; + let o = run(&d, &["run", envelope.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains("W013"), "an envelope: {}", text(&o)); + let o = run(&d, &["check-wasmq", module.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains("W010"), "a module: {}", text(&o)); +} + +/// An envelope of surface(3)'s round and the job of a run of it, in `d`. +fn a_job(d: &Path) -> (PathBuf, PathBuf, String) { + let (prog, prior) = surface3(d); + let env = d.join("round.wai"); + let callout = format!("decode64={DECODER}"); + let prior_arg = format!("{DECODER}={}", prior.display()); + assert!(run(d, &["pack-wasmq", prog.to_str().unwrap(), "--callout", &callout, "--prior", &prior_arg, "--shots", "4", "-o", env.to_str().unwrap()]).status.success()); + let o = run(d, &["run", env.to_str().unwrap(), "--json", "--demo-key"]); + assert!(o.status.success(), "{}", text(&o)); + let job = String::from_utf8(o.stdout).unwrap(); + let jf = d.join("job.json"); + std::fs::write(&jf, &job).unwrap(); + (env, jf, job) +} + +#[test] +fn a_flag_that_does_not_apply_to_an_envelope_or_a_program_is_refused_not_ignored() { + let d = scratch("flags"); + let (env, jf, _) = a_job(&d); + let (e, j) = (env.to_str().unwrap(), jf.to_str().unwrap()); + // A wasmQ job carries no history, so a check of one cannot hold. + let o = run(&d, &["verify", j, "--history", "/nonexistent/history.json"]); + assert!(!o.status.success() && text(&o).contains("history"), "verify --history: {}", text(&o)); + // Each flag of a program's run that an envelope has no use for. + let prog = d.join("memory.qasm"); + let p = prog.to_str().unwrap(); + for extra in [ + &["--history", p][..], + &["--noise", "depolarizing_1q=0.01"], + &["--target", "synthetic-line-5"], + &["--observable", "Z0"], + &["--amplitude", "0"], + &["--option", "max_bond=8"], + &["--compile", "1e-8"], + &["--intent", p], + &["--verdict"], + &["--repair"], + &["--program", p], + &["--sweep", "n=1,2"], + &["--mitigate", "readout"], + &["--param", "theta=0.1"], + &["--fabric", "cpu"], + ] { + let mut args = vec!["run", e, "--demo-key"]; + args.extend_from_slice(extra); + let o = run(&d, &args); + assert!(!o.status.success() && text(&o).contains(extra[0]), "run ENVELOPE {extra:?}: {}", text(&o)); + } + // And the envelope's own flags, on a program. + std::fs::write(d.join("bell.qasm"), "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n").unwrap(); + let bell = d.join("bell.qasm"); + let b = bell.to_str().unwrap(); + for extra in [&["--engine", "native"][..], &["--transcript", "t.jsonl"], &["--replay", "t.jsonl"]] { + let mut args = vec!["run", b, "--demo-key"]; + args.extend_from_slice(extra); + let o = run(&d, &args); + assert!(!o.status.success() && text(&o).contains(extra[0]), "run PROGRAM {extra:?}: {}", text(&o)); + } +} + +#[test] +fn a_job_is_read_from_its_text_as_every_job_is() { + let d = scratch("strict"); + let (env, jf, job) = a_job(&d); + let (e, j) = (env.to_str().unwrap(), jf.to_str().unwrap()); + let o = run(&d, &["verify", j, "--bundle", e]); + assert!(o.status.success(), "the positive control: {}", text(&o)); + // A member twice: a reader that takes the first one reads other counts than are sealed. + let twice = job.replacen("\"counts\":", "\"counts\":{\"000000000\":4},\"counts\":", 1); + assert_ne!(twice, job); + std::fs::write(&jf, &twice).unwrap(); + let o = run(&d, &["verify", j, "--bundle", e]); + assert!(!o.status.success(), "a member twice: {}", text(&o)); + let o = run(&d, &["verify", "--json", j]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("one JSON verdict: {}", text(&o))); + assert_eq!(said["reasons"], serde_json::json!(["unreadable"]), "{said}"); + // A whole number past what a double holds exactly, sealed: readers differ on its value. + let mut v: serde_json::Value = serde_json::from_str(&job).unwrap(); + v["request"]["wasmq"]["run"]["seed"] = serde_json::json!(18446744073709551615u64); + v["result"]["seed"] = serde_json::json!(18446744073709551615u64); + let mut result = v["result"].clone(); + result.as_object_mut().unwrap().remove("receipt"); + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[7; 32]); + v["result"]["receipt"] = wai_quantum::engine::receipt::seal(&key, &v["request"], &result); + let text_of = v.to_string().replace("18446744073709551615", "18446744073709551617"); + std::fs::write(&jf, &text_of).unwrap(); + let o = run(&d, &["verify", "--json", j]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("one JSON verdict: {}", text(&o))); + assert_eq!(said["reasons"], serde_json::json!(["inexact_number", "wasmq_form"]), "a whole number past 2^64, and past the profile's 2^53 - 1: {said}"); + // The shots written 4.0: the same number, sealed as one (the provider protocol's §8.2), + // so the same job, as every other reader of a job finds it. + std::fs::write(&jf, job.replacen("\"shots\":4", "\"shots\":4.0", 1)).unwrap(); + let o = run(&d, &["verify", j, "--bundle", e]); + assert!(o.status.success(), "a number written with a fraction is the same sealed number: {}", text(&o)); +} + +/// `--json` gives one JSON verdict for a wasmQ job, with the bundle's part beside the run +/// when it is given, and `--lines` one verdict for a file of one job; `--history` is +/// refused for a wasmQ job however the verdict is asked for. +#[test] +fn json_and_lines_give_a_wasmq_job_s_verdict_as_json() { + let d = scratch("json"); + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance"); + let job = dir.join("receipts/jobs/cli-wasmq-fault.json"); + let (j, ours, other) = (job.to_str().unwrap(), dir.join("wasmq/envelopes/fault-fuel.wai"), dir.join("wasmq/envelopes/fault-in-init.wai")); + let o = run(&d, &["verify", "--json", j]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("one JSON verdict: {}", text(&o))); + assert!(o.status.success() && said["valid"] == true && said["wasmq"]["outcome"] == "fault", "{said}"); + let o = run(&d, &["verify", "--json", j, "--bundle", ours.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("one JSON verdict: {}", text(&o))); + assert!(o.status.success() && said["valid"] == true, "{said}"); + assert_eq!((said["wasmq"]["bundle"].as_bool(), said["wasmq"]["reproduced"].as_bool()), (Some(true), Some(true)), "{said}"); + let o = run(&d, &["verify", "--json", j, "--bundle", other.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("one JSON verdict: {}", text(&o))); + assert!(!o.status.success() && said["valid"] == false && said["wasmq"]["bundle"] == false, "{said}"); + // Without --json, the same: another envelope fails the check, and says so. + let o = run(&d, &["verify", j, "--bundle", other.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains("NOT the envelope the receipt binds"), "{}", text(&o)); + // One job in a file of lines is one verdict, as JSON. + let one = d.join("one.jsonl"); + let parsed: Value = serde_json::from_slice(&std::fs::read(&job).unwrap()).unwrap(); + std::fs::write(&one, format!("{parsed}\n")).unwrap(); + let o = run(&d, &["verify", "--lines", one.to_str().unwrap()]); + let out = String::from_utf8_lossy(&o.stdout).to_string(); + assert!(o.status.success() && out.lines().count() == 1, "{}", text(&o)); + let said: Value = serde_json::from_str(out.trim()).unwrap_or_else(|_| panic!("a JSON verdict: {out}")); + assert_eq!(said["wasmq"]["outcome"], "fault", "{said}"); + for json in [false, true] { + let mut args = vec!["verify", j, "--history", "/nonexistent/history.json"]; + if json { + args.insert(1, "--json"); + } + let o = run(&d, &args); + assert!(!o.status.success() && text(&o).contains("a wasmQ job carries none"), "--json {json}: {}", text(&o)); + } +} + +// On Linux, where a sparse file costs nothing: a job file past any job's size is +// `too_large` by its size (the provider protocol's section 8.4), before it is read whole and +// before its kind is known; a file of lines past it is refused. +#[cfg(target_os = "linux")] +#[test] +fn verify_gives_a_file_past_any_job_s_size_its_verdict_before_it_is_read() { + let d = scratch("huge-job"); + let path = d.join("huge.json"); + let file = std::fs::File::create(&path).unwrap(); + std::io::Write::write_all(&mut &file, b"{").unwrap(); + if file.set_len(1 << 42).is_err() { + return; // this file system holds no file so long + } + let o = run(&d, &["verify", "--json", path.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("{}", text(&o))); + assert!(!o.status.success() && said["reasons"] == serde_json::json!(["too_large"]) && said["read_at_most"] == serde_json::json!({ "bytes": 384u64 << 20, "values": 1u64 << 22 }), "{said}"); + let o = run(&d, &["verify", path.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains("INVALID (too_large)") && text(&o).contains("not judged"), "{}", text(&o)); + let o = run(&d, &["verify", "--lines", path.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains("bytes verify reads"), "{}", text(&o)); +} + +/// A job refused before its form is read (unreadable text, no receipt) is reported alone: +/// `verify` says its form was not read, runs no envelope for it, and `--json --bundle` +/// adds no statement of a run it did not judge. +#[test] +fn a_job_not_judged_is_reported_alone() { + let d = scratch("not-judged"); + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance"); + let env = dir.join("wasmq/envelopes/fault-fuel.wai"); + let given = std::fs::read_to_string(dir.join("receipts/jobs/cli-wasmq-fault.json")).unwrap(); + let parsed: Value = serde_json::from_str(&given).unwrap(); + let compact = parsed.to_string(); + let twice = compact.replacen("\"counts\":", "\"counts\":{\"0\":32},\"counts\":", 1); + let mut bare = parsed.clone(); + bare["result"].as_object_mut().unwrap().remove("receipt"); + for (what, body, reason) in [("a member named twice", twice, "unreadable"), ("no receipt", bare.to_string(), "no_receipt")] { + let jf = d.join("job.json"); + std::fs::write(&jf, &body).unwrap(); + let o = run(&d, &["verify", jf.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + let out = text(&o); + assert!(!o.status.success() && out.contains("not judged"), "{what}: {out}"); + assert!(!out.contains("a job the profile writes") && !out.contains("reproduced") && out.contains("bundle not run"), "{what}: {out}"); + let o = run(&d, &["verify", "--json", jf.to_str().unwrap(), "--bundle", env.to_str().unwrap()]); + let said: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("{what}: {}", text(&o))); + assert_eq!(said["reasons"], serde_json::json!([reason]), "{what}: {said}"); + assert!(said.get("wasmq").is_none(), "{what}: no statement of a run not judged: {said}"); + } +} diff --git a/wai/wai-quantum-desktop/src/main.rs b/wai/wai-quantum-desktop/src/main.rs index 1bc4c8362..3ab8bac5f 100644 --- a/wai/wai-quantum-desktop/src/main.rs +++ b/wai/wai-quantum-desktop/src/main.rs @@ -1384,8 +1384,9 @@ mod tests { assert_eq!((g["qubits"].as_u64(), g["gates"].as_u64()), (Some(3), Some(3)), "{g}"); let d: Value = serde_json::from_str(&draw("qubit[2] q; h q[0]; cx q[0], q[1];".into(), 80)).unwrap(); assert!(d["text"].as_str().unwrap().contains('⊕')); + // No job: that reason alone, a verdict as on every surface. let v: Value = serde_json::from_str(&verify("{}".into())).unwrap(); - assert!(v["error"].is_string()); + assert_eq!((v["valid"].as_bool(), v["reasons"].clone()), (Some(false), json!(["not_a_job"])), "{v}"); } #[test] @@ -1433,5 +1434,14 @@ mod tests { assert_eq!(said("crafted-read-duplicate.json")["reasons"], json!(["unreadable"])); assert_eq!(said("crafted-number-inexact.json")["reasons"], json!(["inexact_number"])); assert_eq!(said("crafted-provider-measured-simulator.json")["reasons"], json!(["provider_class"])); + // A wasmQ job: held to its profile's form (quantum-wasmq §6.5), its run stated beside. + assert_eq!(said("cli-wasmq-run.json")["valid"], true); + assert_eq!(said("crafted-wasmq-counts-short.json")["reasons"], json!(["wasmq_form"])); + assert_eq!(said("crafted-wasmq-envelope-changed.json")["wasmq"]["outcome"], "fault"); + // Text that does not read is a verdict, as on every surface: unreadable, not an error. + for text in ["not json".to_string(), "\u{feff}{}".to_string()] { + let v: Value = serde_json::from_str(&verify(text)).unwrap(); + assert_eq!((v["valid"].as_bool(), v["reasons"].clone()), (Some(false), json!(["unreadable"])), "{v}"); + } } } diff --git a/wai/wai-quantum-py/python/wai_quantum/_api.py b/wai/wai-quantum-py/python/wai_quantum/_api.py index db8520b9e..6a5814767 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -209,20 +209,49 @@ def _job_of(job: Any) -> Mapping[str, Any]: raise TypeError(f"a job is a dict, JSON text or a path to a JSON file, not {type(job).__name__}") +def _names_a_file(s: str) -> bool: + """Whether a str names a file here: not text that fails as a name (too long, a NUL).""" + try: + return os.path.isfile(s) + except (OSError, ValueError): + return False + + +def _utf8(s: str) -> bytes: + """A str's UTF-8 bytes; a lone surrogate, which no UTF-8 holds, as the bytes that are + not UTF-8, so the text is unreadable as a reader of its bytes finds it.""" + return s.encode("utf-8", "surrogatepass") + + def verify(job: Any) -> Verification: """Whether a job's receipt seals its request and its result, and every rule of the provider protocol's section 8.4 holds, checked here, offline, as every surface checks it: a job run here, by the command line, by the service or in a browser tab. ``job`` is a dict (``{"request": ..., - "result": ...}``), JSON text or its bytes, or a path. Text and a path are - read as the protocol reads them, so a member named twice is found, which a - dict can no longer show. ``reasons`` names what is wrong.""" + "result": ...}``), JSON text or its bytes, or a path: a str is a path when + it names a file, and the job's text otherwise. Text and a path are read as + the protocol reads them, so a member named twice is found, which a dict can + no longer show. Every str and dict gets a verdict (text that does not read + is ``unreadable``), never an error. ``reasons`` names what is wrong.""" if isinstance(job, (bytes, bytearray)): return Verification(_engine.verify_text(bytes(job))) - if isinstance(job, str) and job.lstrip().startswith("{"): - return Verification(_engine.verify_text(job.encode("utf-8"))) + # A str is a path when it names a file, and otherwise the job's text, whatever it begins + # with: text that is no JSON, or no job, is a verdict like any other, never an error. + if isinstance(job, str) and not _names_a_file(job): + return Verification(_engine.verify_text(_utf8(job))) if isinstance(job, (str, os.PathLike)): return Verification(_engine.verify_text(Path(job).read_bytes())) + if isinstance(job, Mapping): + # A dict is judged as its text is, so that what a dict can show gets the verdict + # every surface gives: nesting deeper than a job's text holds is unreadable, a + # number past 64 bits is a number, read as its double, and a lone surrogate (which + # json.loads makes of its escape) is written as that escape, which reads as + # unreadable (section 8.2). A dict that is no JSON at all is unreadable too. + try: + text = json.dumps(job, ensure_ascii=True) + except (RecursionError, TypeError, ValueError): + return Verification({"valid": False, "reasons": ["unreadable"], "key": None, "form": None, "signer": None}) + return Verification(_engine.verify_text(text.encode("ascii"))) return Verification(_engine.verify(_job_of(job))) diff --git a/wai/wai-quantum-py/tests/test_engine.py b/wai/wai-quantum-py/tests/test_engine.py index 3a09e2131..e50c3d50d 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -231,8 +231,9 @@ def test_any_change_to_a_sealed_job_breaks_its_receipt(): assert not wq.verify(forged), path no_receipt = json.loads(json.dumps(job)) del no_receipt["result"]["receipt"] - with pytest.raises(wq.EngineError, match="no receipt"): - wq.verify(no_receipt) + # A result with no receipt is a verdict, that reason alone, as on every surface. + v = wq.verify(no_receipt) + assert not v.valid and v.reasons == ["no_receipt"] def test_seal_and_verify_round_trip_and_the_digest_is_the_receipts(): @@ -572,6 +573,47 @@ def test_a_dict_is_held_to_every_rule_it_can_show(): assert wq.verify(twice.read_text()).reasons == ["unreadable"] +def test_a_dict_is_judged_as_its_text_is(): + """What a dict can show gets the verdict every surface gives: a body nested deeper + than a job's text holds is unreadable, and a number past 64 bits in a member nothing + seals is a number like any other. Text that begins with a byte order mark is read as + text, and is unreadable, not taken for a path.""" + corpus = pathlib.Path(__file__).resolve().parents[2] / "quantum-conformance" / "receipts" / "jobs" + job = json.loads((corpus / "cli-wasmq-run.json").read_text()) + assert wq.verify(job).valid, "the positive control" + nested = 0 + for _ in range(128): + nested = [nested] + deep = dict(job, note=nested) + assert wq.verify(deep).reasons == ["unreadable"] + assert wq.verify(dict(job, id=2 ** 64 + 1)).valid + assert wq.verify("\ufeff" + json.dumps(job)).reasons == ["unreadable"] + + +def test_every_str_and_dict_gets_a_verdict(tmp_path, monkeypatch): + """verify never raises for a str or a dict: a str is a path when it names a file and + the job's text otherwise, whatever it begins with; a lone surrogate, in text or in a + dict json.loads made of its escape, is unreadable; a dict that is no JSON at all is + unreadable. A path that begins with `[` is still a path.""" + corpus = pathlib.Path(__file__).resolve().parents[2] / "quantum-conformance" / "receipts" / "jobs" + text = (corpus / "cli-wasmq-run.json").read_text() + for given, want in [ + ("null", ["not_a_job"]), ("5", ["not_a_job"]), ('"job"', ["not_a_job"]), ("", ["unreadable"]), + (" \r\n\t", ["unreadable"]), ("\x0c" + text, ["unreadable"]), ("\u00a0" + text, ["unreadable"]), + ("\u2028" + text, ["unreadable"]), ("\x00" + text, ["unreadable"]), (" " * 5000 + "x", ["unreadable"]), + ("signature VALID\n", ["unreadable"]), ('{"status": "\ud800"}', ["unreadable"]), + ("no-such-job.json", ["unreadable"]), + ]: + assert wq.verify(given).reasons == want, (given[:40], wq.verify(given).reasons) + lone = json.loads(text.replace('"status":"ok"', '"status":"\\ud800"', 1)) + assert lone["result"]["status"] == "\ud800", "json.loads makes a lone surrogate of its escape" + assert wq.verify(lone).reasons == ["unreadable"], "a dict with a lone surrogate" + assert wq.verify({"request": {}, "result": {"x": {1, 2}}}).reasons == ["unreadable"], "a dict that is no JSON" + monkeypatch.chdir(tmp_path) + (tmp_path / "[run].json").write_text(text) + assert wq.verify("[run].json").valid, "a path that begins with [" + + def test_a_set_s_text_is_judged_as_a_set(): """A genuine set, given as text, a path or bytes, is valid.""" corpus = pathlib.Path(__file__).resolve().parents[2] / "quantum-conformance" / "receipts" / "jobs" diff --git a/wai/wai-quantum-qpu/bring-your-own-quantum.md b/wai/wai-quantum-qpu/bring-your-own-quantum.md index 112e802bd..c8339ebd8 100644 --- a/wai/wai-quantum-qpu/bring-your-own-quantum.md +++ b/wai/wai-quantum-qpu/bring-your-own-quantum.md @@ -189,6 +189,16 @@ The first job on a new provider is the probe, an `X` on qubit 0 whose counts must all land on `…01`. It settles the bit order before any other result is trusted. +## Programs that call WebAssembly between gates + +A wasmQ envelope ([quantum-wasmq](../extensions/quantum-wasmq.md)) calls its WebAssembly +modules between gates: a decoder between a syndrome and its correction, a planner between +loading atoms and moving them. A provider's program is a fixed sequence of native gates +measured at the end, so it cannot carry that, and `run ENVELOPE.wai --provider NAME` is +refused; the envelope runs on this machine, its quantum part simulated. Nothing about +bringing your own quantum changes for it. What a hardware run of such a program would +need, and what replays from its record, is §7 of that extension. + ## What the receipt proves, and what it does not A processor's result is labelled `measured`. A result from a provider's diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index 27fde1a4c..d16473d7a 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -145,7 +145,7 @@ Counts are always seeded samples. | `GET /v1/jobs/{id}` | the job: its status, the request as submitted, its result when it is done, and the digests it is kept under; `?wait=S` (at most 25) holds the answer until its status changes | | `GET /v1/jobs/{id}/events` | the job's status as server-sent events: a `status` event at each change, then one `job` event with the whole job when it ends | | `DELETE /v1/jobs/{id}` | cancel a queued or running job | -| `POST /v1/verify` | `{"request": ..., "result": ...}`: whether the result's receipt seals that request and that result and every rule of [the provider protocol's section 8.4](../extensions/quantum-providers.md#84-what-a-verifier-checks) holds, read from the body's own text: `valid`, `reasons`, `key`, `form`, and what a provider's result says of itself | +| `POST /v1/verify` | `{"request": ..., "result": ...}`: whether the result's receipt seals that request and that result and every rule of [the provider protocol's section 8.4](../extensions/quantum-providers.md#84-what-a-verifier-checks) holds, read from the body's own text: `valid`, `reasons`, `key`, `form`, and what a provider's result says of itself. A body that does not read as a job's, is no job, has no receipt, or is longer than the 4 MiB and 2^19 values it reads (`too_large`, stating both) gets that reason as its verdict; past them, `wai-quantum verify` reads further. Each check runs off the request's thread, at most two at once; an estimate or an algorithm's answer whose request runs again nowhere is not valid, and says why (`rerun_error`). An estimate or an algorithm's answer whose receipt holds is also run again | | `POST /v1/critique` | `{"qasm"}`: the program's measures (gates by kind, two-qubit gates, depth, T count, whether it is Clifford, a modelled fidelity) and its smells, each with its lines | | `POST /v1/equivalent` | `{"a", "b"}`: whether two programs are the same circuit up to a global phase, and how that was decided | | `POST /v1/convert` | `{"to", "program" \| "program_base64", "from"?, "profile"?, "check"?}`: the program carried between OpenQASM 3, QIR and jeff (as base64), with the program as OpenQASM 3, each note with its code, what was read and written, and the check (the output read back and held to the program by the equivalence ladder); a refusal is the reader's, with its code and place ([the formats page](../wai-quantum/docs/formats.md)) | diff --git a/wai/wai-quantum-service/docs/self-host.md b/wai/wai-quantum-service/docs/self-host.md index 4a82dfc22..30c4dc09d 100644 --- a/wai/wai-quantum-service/docs/self-host.md +++ b/wai/wai-quantum-service/docs/self-host.md @@ -130,7 +130,7 @@ Run the service on a loopback address and let the proxy take the public traffic. - **The client's address.** The proxy sets `X-Forwarded-For`, and the service trusts it only from `WAI_QSVC_TRUSTED_PROXIES`. Have the proxy set the header from the connection itself, not pass on what the request carried, or a client could choose its own limits. - **A path prefix.** Mount the service under a prefix (the public one is `/quantum-jobs`) by stripping the prefix before passing the request on. Set `WAI_QSVC_PUBLIC_BASE` to the prefixed URL, so the links the service hands out are right. A well-known path belongs at the origin's root: map `/.well-known/wai-quantum-receipt-keys` there to the service's own. - **Event streams.** `GET /v1/jobs/{id}/events` is a server-sent event stream. The proxy must pass it through unbuffered (the service sends `X-Accel-Buffering: no` and a comment every 15 s), and allow it to stay open as long as `WAI_QSVC_JOB_SECS`. Polls with `?wait=` are held up to 25 s. -- **Bodies.** A request body is at most 1 MiB, and `POST /v1/run` waits up to 25 s before answering 202. +- **Bodies.** A request body is at most 1 MiB, and `POST /v1/run` waits up to 25 s before answering 202. A job `POST /v1/verify` and `/mcp` check is at most 4 MiB and 2^19 values (`too_large` past them); each check holds some 50 MiB, off the async workers, and at most two run at once. - **Rate-limit fields.** Pass the `RateLimit-Policy`, `RateLimit` and `Retry-After` headers through. A minimal site, in one common proxy's configuration format: diff --git a/wai/wai-quantum-service/schemas/service-tools.v1.json b/wai/wai-quantum-service/schemas/service-tools.v1.json index e06c0aa2e..f0654bfbf 100644 --- a/wai/wai-quantum-service/schemas/service-tools.v1.json +++ b/wai/wai-quantum-service/schemas/service-tools.v1.json @@ -51,7 +51,7 @@ "items": { "type": "string" }, - "description": "Each rule the receipt breaks (the provider protocol's section 8.4): none when it is valid." + "description": "Each rule the receipt breaks (the provider protocol's section 8.4, wasmq_form among them for a wasmQ job): none when it is valid." }, "form": { "type": [ @@ -76,6 +76,39 @@ "null" ], "description": "That key as a did:key." + }, + "wasmq": { + "type": "object", + "description": "For a wasmQ job (quantum-wasmq section 6.5), beside the verdict: how the run ended, each rule of the profile's form it breaks, and where the host's unsealed words differ from what is sealed, which change no verdict.", + "required": [ + "outcome", + "problems", + "notes" + ], + "additionalProperties": false, + "properties": { + "outcome": { + "type": [ + "string", + "null" + ], + "description": "The sealed result's status: ok, or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Each rule of quantum-wasmq section 6.5 the job breaks, in words (the reason wasmq_form): the first 64, then how many more. Empty when it breaks none." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the body's unsealed status or method differs from what is sealed or is missing, and members nothing seals: told beside the verdict, and no part of it." + } + } } } }, diff --git a/wai/wai-quantum-service/src/analysis.rs b/wai/wai-quantum-service/src/analysis.rs index fff9cb89c..d3a92f060 100644 --- a/wai/wai-quantum-service/src/analysis.rs +++ b/wai/wai-quantum-service/src/analysis.rs @@ -122,13 +122,28 @@ pub fn compile_bounds(qasm: &str, options: &Value) -> Result<(), (u16, String)> /// "budget", …}`): the reason it does not, with the status to answer, or /// nothing. pub fn estimate_bounds(body: &Value) -> Result<(), (u16, String)> { + estimate_fits(body, estimate_size(body)?) +} + +/// [`estimate_bounds`] for an estimate sealed already and run again to verify it: a request +/// no estimate runs is no refusal here, since running it again gives the verdict that says so +/// (`rerun_error`); one past what the service takes is refused, naming the local command. +pub fn estimate_rerun_bounds(body: &Value) -> Result<(), (u16, String)> { + match estimate_size(body) { + Ok(size) => estimate_fits(body, size), + Err(_) => Ok(()), + } +} + +/// What an estimate of `body` counts: its gates (of a program), its rotations and its +/// distinct angles; or why the request is not one an estimate runs. +fn estimate_size(body: &Value) -> Result<(Option, u64, usize), (u16, String)> { use wai_quantum::engine::estimate::{self, count, interchange}; - let local = "run `wai-quantum estimate` on your machine"; - let o = estimate::Options::read(body).map_err(|e| (400, e))?; - let (rotations, angles) = if let Some(q) = body.get("qasm").and_then(Value::as_str) { + estimate::Options::read(body).map_err(|e| (400, e))?; + if let Some(q) = body.get("qasm").and_then(Value::as_str) { let p = wai_quantum::qasm::parse(q).map_err(|e| (400, e))?; if p.ops.len() > ESTIMATE_MAX_GATES { - return Err((422, format!("{} gates, more than an estimate here counts ({ESTIMATE_MAX_GATES}); {local}", p.ops.len()))); + return Ok((Some(p.ops.len()), 0, 0)); } let low = count::lower_program(&p).map_err(|e| (422, e))?; let mut seen = std::collections::BTreeSet::new(); @@ -141,18 +156,27 @@ pub fn estimate_bounds(body: &Value) -> Result<(), (u16, String)> { seen.insert(g.params[0].to_bits()); } } - (r, seen.len()) + Ok((Some(p.ops.len()), r, seen.len())) } else if let Some(c) = body.get("counts").filter(|v| !v.is_null()) { - (interchange::read_counts(c).map_err(|e| (400, e))?.rotations, estimate::reference_angles().len()) + Ok((None, interchange::read_counts(c).map_err(|e| (400, e))?.rotations, estimate::reference_angles().len())) } else if let Some(r) = body.get("routine").filter(|v| !v.is_null()) { - (interchange::read_routine(r).map_err(|e| (400, e))?.rotations, estimate::reference_angles().len()) + Ok((None, interchange::read_routine(r).map_err(|e| (400, e))?.rotations, estimate::reference_angles().len())) } else { - return Err((400, "give \"qasm\" (the program), \"counts\" (a logical-counts document) or \"routine\" (a routine document)".into())); - }; + Err((400, "give \"qasm\" (the program), \"counts\" (a logical-counts document) or \"routine\" (a routine document)".into())) + } +} + +/// Whether the service takes an estimate of `size`: the refusal, naming the local command. +fn estimate_fits(body: &Value, (gates, rotations, angles): (Option, u64, usize)) -> Result<(), (u16, String)> { + let local = "run `wai-quantum estimate` on your machine"; + if let Some(g) = gates.filter(|&g| g > ESTIMATE_MAX_GATES) { + return Err((422, format!("{g} gates, more than an estimate here counts ({ESTIMATE_MAX_GATES}); {local}"))); + } if angles > ESTIMATE_MAX_ANGLES { return Err((422, format!("{angles} distinct rotation angles to synthesise, more than an estimate here takes ({ESTIMATE_MAX_ANGLES}); {local}"))); } if rotations > 0 { + let o = wai_quantum::engine::estimate::Options::read(body).map_err(|e| (400, e))?; let each = wai_quantum::quantum_resource::Budget::split(o.budget, true, true).synthesis / rotations as f64; if each < ESTIMATE_MIN_EACH { return Err((422, format!("a budget of {:e} over {rotations} rotations leaves {each:e} each, finer than an estimate here synthesises ({ESTIMATE_MIN_EACH:e}); {local}", o.budget))); diff --git a/wai/wai-quantum-service/src/discovery.rs b/wai/wai-quantum-service/src/discovery.rs index e51f1dc9b..579816051 100644 --- a/wai/wai-quantum-service/src/discovery.rs +++ b/wai/wai-quantum-service/src/discovery.rs @@ -161,7 +161,7 @@ pub fn openapi(server: &str) -> Value { "time": { "type": "object", "description": "{\"seconds\", \"basis\"}: an estimate from this service's own timings of the method, or the planner's nominal rate; or {\"seconds\": null, \"why\", \"at_most\"} where the work does not predict the time." }, "energy": { "type": "object", "description": "{\"joules_micro_estimate\", \"energy_provenance\": \"Estimator\", \"basis\"} from the power of past metered jobs of the method here, or {\"unestimated\": why}. Never a reading." }, "within_time_limit": { "type": ["boolean", "null"] }, - "bounds": { "type": "object", "description": "The limits that apply: job_memory_bytes, job_seconds, active_jobs_per_client, memory_per_client_bytes, submissions_per_window, window_seconds, queue, set_items, body_bytes." }, + "bounds": { "type": "object", "description": "The limits that apply: job_memory_bytes, job_seconds, active_jobs_per_client, memory_per_client_bytes, submissions_per_window, window_seconds, queue, set_items, body_bytes, verify_body_bytes and verify_body_values (the bytes and the values of a job POST /v1/verify and /mcp read)." }, "client": { "type": "object", "description": "Where the client stands: active_jobs, memory_held_bytes, submissions_left, window_resets_in_seconds." }, "items": { "type": "array", "description": "A set's items, each with its index, point, method, memory, work, time and energy." } } @@ -282,9 +282,15 @@ pub fn openapi(server: &str) -> Value { "get": { "summary": "The job's status as server-sent events: `status` events ({\"id\", \"status\"}) as it changes, then one `job` event with the whole job when it ends, and the stream closes; a comment every 15 s keeps it open", "responses": { "200": { "description": "An event stream", "content": { "text/event-stream": {} } }, "404": { "description": "No such job" } } } }, "/v1/verify": { "post": { - "summary": "Whether a result's receipt seals that request and that result and every rule of its form holds ({\"valid\", \"reasons\", \"key\", \"form\"}); an estimate is also re-run, and each of its resource receipts checked ({\"valid\", \"signed\", \"receipts\", \"reproduced\"}); a set's receipt is checked over its head, every item as its request gives it, each item's own receipt, the root and each path ({\"valid\", \"signed\", \"derived\", \"items_sealed\", \"root\"})", - "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["request", "result"], "properties": { "request": { "$ref": "#/components/schemas/JobRequest" }, "result": { "type": "object" } } } } } }, - "responses": { "200": { "description": "{\"valid\": bool, \"reasons\": [string]}" }, "400": { "description": "The body is not JSON, not a job, or has no receipt" } } + "summary": "Whether a job's receipt seals its request and its result and every rule of the provider protocol's section 8.4 holds ({\"valid\", \"reasons\", \"key\", \"form\", \"signer\"}), read from the body's text; a wasmQ job's verdict also states its outcome, the rules it breaks and its notes (\"wasmq\"). Any body gets a verdict with 200: text that does not read (unreadable), a body that is no job (not_a_job), a result with no receipt (no_receipt) and a body longer than verify_body_bytes, or holding more values than verify_body_values (too_large, with read_at_most: {\"bytes\", \"values\"}), are each that reason alone. An estimate or an algorithm's answer whose receipt holds is also run again here ({\"valid\", \"reasons\", \"signed\", \"receipts\", \"reproduced\", \"rerun_error\"} for an estimate; {\"valid\", \"reasons\", \"signed\", \"chain\", \"rerun\", \"rerun_error\"} for an algorithm's answer; a request that does not run again anywhere is not valid, and rerun_error says why); a set's receipt is checked over its head, every item as its request gives it, each item's own receipt, the root and each path ({\"valid\", \"reasons\", \"signed\", \"derived\", \"items_sealed\", \"root\"})", + "requestBody": { "required": true, "content": { "application/json": { "schema": { "description": "A job's text: {\"request\", \"result\"}, the result carrying its receipt; any other body is answered with its verdict", "type": "object", "required": ["request", "result"], "properties": { "request": { "$ref": "#/components/schemas/JobRequest" }, "result": { "type": "object" } } } } } }, + "responses": { + "200": { "description": "The verdict, for every body: {\"valid\": bool, \"reasons\": [string], ...}; valid is true exactly when reasons is empty and anything run again agrees" }, + "400": { "description": "The body could not be read off the connection" }, + "422": { "description": "An estimate or an algorithm's answer whose re-run is larger than the service takes; the reason names the command that runs it locally" }, + "429": { "description": "Past the per-client limits" }, + "503": { "description": "Every check is running" } + } } }, "/v1/critique": { "post": { "summary": "A program's measures and smells, each with its lines", @@ -338,7 +344,7 @@ pub fn openapi(server: &str) -> Value { "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string" }, "proposed": { "type": "string", "description": "A repaired version: taken only if it changes no line the diagnostics do not flag." }, "max_iterations": { "type": "integer", "minimum": 1 }, "dyadic_bits": { "type": "integer", "minimum": 1, "maximum": 32 }, "sizes": { "type": "object", "additionalProperties": { "type": "integer" } }, "diagnostics": { "type": "array" }, "rules": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "{\"ok\", \"changed\", \"qasm\", \"repairs\": [{\"code\", \"rule\", \"by\", \"flagged\", \"edits\"}], \"needs_person\", \"verdict\"}, or {\"ok\": false, \"error\", \"refusal\"} for a proposal rejected (J021)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } } } }, - "/mcp": { "post": { "summary": "The Model Context Protocol endpoint (streamable HTTP, JSON responses): tools to run circuits and sets, plan a job without running it, read jobs, verify receipts, list examples, and critique, compare, improve, price, compile, estimate and repair programs", "responses": { "200": { "description": "A JSON-RPC response" } } } } + "/mcp": { "post": { "summary": "The Model Context Protocol endpoint (streamable HTTP, JSON responses): tools to run circuits and sets, plan a job without running it, read jobs, verify receipts, list examples, and critique, compare, improve, price, compile, estimate and repair programs. A call to verify_quantum_receipt gets its verdict whatever its job: past verify_body_bytes or verify_body_values (too_large), or nested past 127 (unreadable), judged before the message is read whole; a message past six times verify_body_bytes is read only as far as its id, method and tool", "responses": { "200": { "description": "A JSON-RPC response" }, "400": { "description": "A message that is not JSON (JSON-RPC -32700) or not one request (-32600)" }, "413": { "description": "A message past what the server reads whose call is not verify_quantum_receipt (JSON-RPC -32600, by its id when it is read)" } } } } } }); for (path, item) in qec_paths().into_iter().chain(pulse_paths()).chain(variational_paths()) { diff --git a/wai/wai-quantum-service/src/http.rs b/wai/wai-quantum-service/src/http.rs index d69720d8b..3b39a0a39 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -98,11 +98,30 @@ use serde_json::{Value, json}; use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; +use wai_quantum::engine::job; use wai_quantum::engine::refusal::{self, tag}; use wai_quantum::engine::sets; /// The largest request body, bytes. pub const MAX_BODY: usize = 1 << 20; +/// The most bytes of a job `POST /v1/verify` and `/mcp` read. A check holds a job's values +/// as a tree, so the service also reads at most [`VERIFY_VALUES`] of them: within both a +/// check holds some 50 MiB (twice its bytes and 75 bytes a value), and at most +/// [`CHECKS_AT_ONCE`] run at once. A longer job is +/// `too_large`, a verdict like any other (the provider protocol's section 8.4), which +/// `wai-quantum verify` reads further. +pub const VERIFY_BODY: usize = 4 << 20; +/// The most values of a job `POST /v1/verify` and `/mcp` read ([`job::values_in`]). +pub const VERIFY_VALUES: u64 = 1 << 19; +/// What the service reads of a job, as a verdict states it. +pub const VERIFY_BOUND: job::Bound = job::Bound { bytes: VERIFY_BODY as u64, values: VERIFY_VALUES }; +/// The most bytes of a message `/mcp` reads whole: a job within [`VERIFY_BODY`] given as a +/// string, each of its bytes written at most six (`\u00XX`), and the call around it. Past +/// it, the message is read only as far as its id, method and tool. +pub const MCP_BODY: usize = 6 * VERIFY_BODY + (64 << 10); +/// The most checks of a job that run at once, each off the service's async workers. +pub const CHECKS_AT_ONCE: usize = 2; +static CHECKS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(CHECKS_AT_ONCE); /// How long `POST /v1/run` waits before answering 202. pub const RUN_WAIT: Duration = Duration::from_secs(25); @@ -202,6 +221,8 @@ async fn describe(State(app): State) -> Response { "components": wai_quantum::engine::registry::Registry::builtins().components(), "limits": { "body_bytes": MAX_BODY, + "verify_body_bytes": VERIFY_BODY, + "verify_body_values": VERIFY_VALUES, "job_memory_bytes": c.job_mib << 20, "job_seconds": c.job_timeout.as_secs(), "active_jobs_per_client": c.per_client_active, @@ -248,6 +269,8 @@ pub(crate) fn bounds(app: &App) -> Value { "queue": c.max_queue, "set_items": sets::MAX_ITEMS, "body_bytes": MAX_BODY, + "verify_body_bytes": VERIFY_BODY, + "verify_body_values": VERIFY_VALUES, }) } @@ -562,40 +585,73 @@ async fn cancel(State(app): State, Path(id): Path) -> Response { /// `{"request": ..., "result": ...}` with the result's own `receipt`: whether /// the receipt seals that request and that result and every rule of the /// provider protocol's section 8.4 holds, read from the body's own text -/// (`engine::job::check_text`): `valid`, `reasons`, `key`, `form`, and what a -/// provider's result says of itself. An estimate is also re-run, and every -/// resource receipt in it checked. +/// (`engine::job::answer_of_text`): `valid`, `reasons`, `key`, `form`, and what a +/// provider's result says of itself. A body that does not read, is no job, has no +/// receipt or is longer than the service reads gets that reason as its verdict. An +/// estimate or an algorithm's answer whose receipt holds is also run again here. async fn verify(State(app): State, ConnectInfo(peer): ConnectInfo, headers: HeaderMap, body: Result) -> Response { - let Ok(bytes) = body else { return error(400, "the body is not JSON") }; - let Some(text) = std::str::from_utf8(&bytes).ok() else { return error(400, "the body is not JSON") }; - let Ok(v) = serde_json::from_str::(text) else { return error(400, "the body is not JSON") }; - // Read as every job's text is, whatever kind of job it is. - if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT && let Some(reason) = wai_quantum::engine::job::text_refusal(text) { - return reply(200, json!({ "valid": false, "reasons": [reason] })); - } - // An estimate: its seal, every resource receipt, and its request re-run - // here, within an estimate's bounds. - if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - let who = client(&app, peer, &headers); - if let Err((code, why)) = crate::analysis::estimate_bounds(&v["request"]) { - return error(code, &why); - } - return match crate::analysis::bounded(&app, &who, move || wai_quantum::engine::estimate::verify(&v)).await { - Ok(checked) => reply(200, checked), - Err((code, why)) => error(code, &why), - }; - } - // A set: its receipt over its head, every item, the root and the paths (§8.6). - if v["result"]["format"] == sets::FORMAT { - return reply(200, wai_quantum::engine::job::verdict_of_text(text).unwrap_or_else(|e| json!({ "valid": false, "error": e }))); - } - let (Some(_), Some(result)) = (v.get("request"), v.get("result")) else { - return error(400, "expected {\"request\": ..., \"result\": ...}, the result carrying its receipt"); + let bytes = match body { + Ok(b) => b, + Err(e) if e.status() == StatusCode::PAYLOAD_TOO_LARGE => return reply(200, job::too_large(VERIFY_BOUND)), + Err(e) => return error(400, &format!("the body could not be read: {e}")), }; - if result.get("receipt").is_none() { - return error(400, "the result carries no receipt"); + // Past the values the service reads, before anything is built. + if !job::within(&bytes, VERIFY_BOUND) { + return reply(200, job::too_large(VERIFY_BOUND)); + } + let who = client(&app, peer, &headers); + match verdict(&app, &who, Given::Bytes(bytes)).await { + Ok(v) => reply(200, v), + Err((code, why)) => error(code, &why), + } +} + +/// A job as a surface is given it: its bytes, its text, or its members already parsed. +pub(crate) enum Given { + Bytes(axum::body::Bytes), + Text(String), + Members(Value), +} + +/// The service's one answer to a job, as every surface gives it (`job::answer_of_text`, +/// `job::answer_of`): its verdict; and for an estimate or an algorithm's answer whose +/// receipt holds, its request run again here within the bounds its own route holds it to. +/// The check runs off the async workers, at most [`CHECKS_AT_ONCE`] at a time, as the +/// run again does ([`crate::analysis::bounded`]). +pub(crate) async fn verdict(app: &App, who: &str, given: Given) -> Result { + let Ok(Ok(turn)) = tokio::time::timeout(crate::analysis::WAIT, CHECKS.acquire()).await else { + return Err((503, format!("{CHECKS_AT_ONCE} checks of a job are running; try again shortly"))); + }; + let checked = tokio::task::spawn_blocking(move || -> Result, (u16, String)> { + let answer = match &given { + Given::Bytes(bytes) => match std::str::from_utf8(bytes) { + Ok(text) => job::answer_of_text(text), + Err(_) => job::Answer::Verdict(job::verdict_of(&job::check_bytes(bytes))), + }, + Given::Text(text) => job::answer_of_text(text), + Given::Members(v) => job::answer_of(v), + }; + match answer { + job::Answer::Verdict(v) => Ok(Ok(v)), + job::Answer::Rerun(j) => { + // A request no host runs again is run again all the same, and its verdict says + // so; one past what the service runs is refused, naming the local command. + if j["result"]["format"] == wai_quantum::engine::estimate::FORMAT { + crate::analysis::estimate_rerun_bounds(&j["request"])?; + } else { + crate::analysis::variational_bounds(j["result"]["call"].as_str().unwrap_or(""), &j["request"])?; + } + Ok(Err(j)) + } + } + }) + .await + .map_err(|e| (500, format!("the check failed: {e}")))?; + drop(turn); + match checked? { + Ok(v) => Ok(v), + Err(j) => crate::analysis::bounded(app, who, move || job::rerun(&j)).await, } - reply(200, wai_quantum::engine::job::check_text(text).to_json()) } /// A design check ([`crate::analysis`]): the engine's answer, or the refusal. @@ -756,13 +812,38 @@ async fn index(State(app): State, headers: HeaderMap) -> Response { /// (`wq-views-…`, random, held nowhere), and every request that carries it is /// answered with the views (see `mcp`); any other client gets no session id /// and no views. -async fn mcp(State(app): State, ConnectInfo(peer): ConnectInfo, headers: HeaderMap, body: Result, axum::extract::rejection::JsonRejection>) -> Response { - let Ok(Json(msg)) = body else { - return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": "not JSON" } })); +async fn mcp(State(app): State, ConnectInfo(peer): ConnectInfo, headers: HeaderMap, body: axum::body::Body) -> Response { + let past = |id: Value| json!({ "jsonrpc": "2.0", "id": id, "error": { "code": -32600, "message": format!("the call is past what this server reads ({VERIFY_BODY} bytes and {VERIFY_VALUES} values of a call's arguments, {MCP_BODY} bytes of a message); a job past it gets its verdict from `wai-quantum verify`") } }); + let (bytes, cut) = match read_at_most(body, MCP_BODY).await { + Ok(b) => b, + Err(e) => return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("the message could not be read: {e}") } })), + }; + if cut { + // Read as far as it says what it is: a verify call gets its verdict, too_large. + let (id, method, name) = job::message_head(&bytes); + return match id { + Some(id) if method.as_deref() == Some("tools/call") && name.as_deref() == Some(crate::mcp::VERIFY_TOOL) => reply(200, crate::mcp::verdict_reply(&id, job::too_large(VERIFY_BOUND))), + id => reply(413, past(id.unwrap_or(Value::Null))), + }; + } + // Read so that a job (its text, the job, or a call's arguments as its members) gets its + // verdict whatever it is: past what the service reads it is too_large, and nested past + // what a reader takes unreadable, without the message read whole. + let (msg, judged) = match job::read_carrying(&bytes, &[&["params", "arguments", "job"], &["params", "arguments"]], Some(VERIFY_BOUND)) { + Ok(read) => read, + Err(e) => return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })), }; if !msg.is_object() { return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "one JSON-RPC message a request" } })); } + if let Some(verdict) = judged { + let id = msg.get("id").cloned().unwrap_or(Value::Null); + return if msg["method"] == "tools/call" && msg["params"]["name"] == crate::mcp::VERIFY_TOOL { + reply(200, crate::mcp::verdict_reply(&id, verdict)) + } else { + reply(200, past(id)) + }; + } let who = client(&app, peer, &headers); let opening = msg["method"] == "initialize" && wai_quantum::engine::views::negotiated(&msg["params"]); let views = opening || headers.get("mcp-session-id").and_then(|v| v.to_str().ok()).is_some_and(crate::mcp::is_views_session); @@ -806,6 +887,23 @@ async fn mcp(State(app): State, ConnectInfo(peer): ConnectInfo Result<(Vec, bool), String> { + use futures_util::StreamExt; + let mut stream = body.into_data_stream(); + let mut out = Vec::new(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| e.to_string())?; + if out.len() + chunk.len() > cap { + out.extend_from_slice(&chunk[..cap - out.len()]); + return Ok((out, true)); + } + out.extend_from_slice(&chunk); + } + Ok((out, false)) +} + /// No event stream is offered: `GET /mcp` is 405, as the protocol allows. async fn mcp_get() -> Response { let mut r = error(405, "this endpoint takes POST; it offers no event stream"); @@ -875,7 +973,7 @@ pub fn router(app: Shared) -> Router { .route("/v1/jobs/{id}/events", get(events)) .route("/.well-known/wai-quantum-receipt-keys", get(receipt_keys)) .route("/v1/run", post(run)) - .route("/v1/verify", post(verify)) + .route("/v1/verify", post(verify).layer(DefaultBodyLimit::max(VERIFY_BODY))) .route("/v1/critique", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Critique))) .route("/v1/equivalent", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Equivalent))) .route("/v1/improve", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Improve))) diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index 6c3265fce..9b9376eea 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -287,7 +287,7 @@ fn tool_list() -> Value { "name": "verify_quantum_receipt", "title": "Verify a result's receipt", "description": "Whether a result's receipt (an Ed25519 signature) seals exactly that request and that result, and every rule of its form holds (`reasons` names what does not). Pass the job's own `request` (as run_quantum_circuit or get_quantum_job returns it: a run here asks for its verdict, so the request it seals says so) and its result.", - "inputSchema": { "type": "object", "properties": { "job": { "type": "string", "description": "The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen)" }, "request": { "type": "object" }, "result": { "type": "object" } } }, + "inputSchema": { "type": "object", "properties": { "job": { "type": ["string", "object"], "description": "The job's text as it was given (preferred: read as the protocol reads it, so a member named twice or a number past what a parsed job keeps is seen), or the job as an object" }, "request": { "type": "object" }, "result": { "type": "object" }, "status": { "description": "The body's status as the host gave it: unsealed, and noted where it differs from what is sealed" }, "method": { "description": "The body's method as the host gave it: unsealed, and noted where it differs from what is sealed" } } }, "annotations": { "readOnlyHint": true } }, { @@ -454,6 +454,31 @@ fn fail(id: &Value, code: i64, message: &str) -> Value { json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) } +/// The tool that verifies a job. +pub const VERIFY_TOOL: &str = "verify_quantum_receipt"; + +/// The verify tool's answer of a verdict: the verdict in words, and whole. +fn verdict_result(v: Value) -> Value { + let text = if v["valid"] == true { + "valid: the receipt seals exactly this request and this result, and every rule of its form holds".to_string() + } else if let Some(e) = v["rerun_error"].as_str() { + format!("not valid: the request is not one that runs again here or anywhere: {e}") + } else if v.get("reproduced").is_some() { + format!("not valid: reasons {}, signed {}, receipts {}, reproduced {}", v["reasons"], v["signed"], v["receipts"], v["reproduced"]) + } else if v.get("rerun").is_some() { + format!("not valid: reasons {}, signed {}, run again to the same bytes {}", v["reasons"], v["signed"], v["rerun"]) + } else { + format!("not valid: {}", v["reasons"]) + }; + tool_result(text, v, false) +} + +/// A call of the verify tool answered with `verdict`, given before its message was read +/// whole (a job past what the service reads, or nested past what a reader takes). +pub fn verdict_reply(id: &Value, verdict: Value) -> Value { + ok(id, verdict_result(verdict)) +} + /// A tool's answer: text for the model, the whole object as structured content. fn tool_result(text: String, structured: Value, is_error: bool) -> Value { json!({ "content": [{ "type": "text", "text": text }], "structuredContent": structured, "isError": is_error }) @@ -801,41 +826,25 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu } Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true), }, - "verify_quantum_receipt" if args["result"]["format"] == wai_quantum::engine::estimate::FORMAT => { - if let Err((_, why)) = crate::analysis::estimate_bounds(&args["request"]) { - return tool_result(format!("refused: {why}"), json!({ "error": why }), true); - } - let body = args.clone(); - match crate::analysis::bounded(app, who, move || wai_quantum::engine::estimate::verify(&body)).await { - Ok(v) => tool_result(if v["valid"] == true { "valid: the seal, every estimate's receipt, and the request re-run here all hold".into() } else { format!("not valid: signed {}, receipts {}, reproduced {}", v["signed"], v["receipts"], v["reproduced"]) }, v, false), + "verify_quantum_receipt" => { + // The job's text (read as the protocol's section 8.2 reads it), the job as an + // object, or its members as given, the host's unsealed ones among them: each gets + // the answer every surface gives (`http::verdict`), an estimate or an algorithm's + // answer whose receipt holds run again here. + let given = match &args["job"] { + Value::String(text) => crate::http::Given::Text(text.clone()), + job @ Value::Object(_) => crate::http::Given::Members(job.clone()), + _ => { + let mut body = args.as_object().cloned().unwrap_or_default(); + body.remove("job"); + crate::http::Given::Members(Value::Object(body)) + } + }; + match crate::http::verdict(app, who, given).await { + Ok(v) => verdict_result(v), Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true), } } - "verify_quantum_receipt" if args["result"]["format"] == wai_quantum::engine::sets::FORMAT => { - let v = wai_quantum::engine::sets::verify(&args); - let text = if v["valid"] == true { - format!("valid: the set's receipt seals its request and head, and every one of its {} items is sealed by its own receipt and its path to the root", v["items"]) - } else { - format!("not valid: signed {}, items as the request gives them {}, each item sealed {}, root {}", v["signed"], v["derived"], v["items_sealed"], v["root"]) - }; - tool_result(text, v, false) - } - "verify_quantum_receipt" if args["job"].is_string() => { - // The job's text, read as the protocol's section 8.2 reads it. - let text = args["job"].as_str().unwrap_or_default(); - let v = wai_quantum::engine::job::verdict_of_text(text).unwrap_or_else(|_| wai_quantum::engine::job::verdict_of(&wai_quantum::engine::job::check_text(text))); - let line = if v["valid"] == true { "valid: the receipt seals exactly this request and this result, and every rule of its form holds".to_string() } else { format!("not valid: {}", v["reasons"]) }; - tool_result(line, v, false) - } - "verify_quantum_receipt" => { - let (request, result) = (&args["request"], &args["result"]); - if result.get("receipt").is_none() { - return tool_result("the result carries no receipt".into(), json!({ "valid": false }), true); - } - let c = wai_quantum::engine::job::check(&json!({ "request": request, "result": result })); - let text = if c.valid() { "valid: the receipt seals exactly this request and this result, and every rule of its form holds".to_string() } else { format!("not valid: {}", c.reasons.join(", ")) }; - tool_result(text, c.to_json(), false) - } "quantum_service_info" => { let c = &app.service.config; let (queued, running, _) = app.service.load(); @@ -1169,7 +1178,7 @@ mod tests { // Given the job's text, its answer names the signer as a did:key too. ("verify_quantum_receipt", json!({ "job": body.to_string() }), false), ("verify_quantum_receipt", json!({ "request": body["request"], "result": forged }), false), - ("verify_quantum_receipt", json!({ "request": body["request"], "result": { "counts": {} } }), true), + ("verify_quantum_receipt", json!({ "request": body["request"], "result": { "counts": {} } }), false), ("critique_qasm", json!({ "qasm": bell }), false), ("critique_qasm", json!({}), true), ("check_equivalent", json!({ "a": bell, "b": "OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\n" }), false), diff --git a/wai/wai-quantum-service/tests/verify.rs b/wai/wai-quantum-service/tests/verify.rs index d7c984b87..5ad3c8ad8 100644 --- a/wai/wai-quantum-service/tests/verify.rs +++ b/wai/wai-quantum-service/tests/verify.rs @@ -25,6 +25,31 @@ fn http(port: u16, method: &str, path: &str, body: &[u8]) -> (u16, String) { (status, out.split_once("\r\n\r\n").map(|(_, b)| b.to_string()).unwrap_or_default()) } +/// [`http`] of a body the service stops reading: written from another thread, so that the +/// answer is read whenever it comes, and a connection the service closes on the rest of the +/// body is no failure of the exchange. +fn http_past_its_limit(port: u16, path: &str, body: Vec) -> (u16, String) { + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); + s.set_read_timeout(Some(Duration::from_secs(120))).unwrap(); + let mut w = s.try_clone().unwrap(); + let head = format!("POST {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len()); + let writer = std::thread::spawn(move || { + let _ = w.write_all(head.as_bytes()).and_then(|()| w.write_all(&body)); + }); + let mut out = Vec::new(); + let mut buf = [0u8; 1 << 16]; + while let Ok(n) = s.read(&mut buf) { + if n == 0 { + break; + } + out.extend_from_slice(&buf[..n]); + } + let _ = writer.join(); + let out = String::from_utf8_lossy(&out).into_owned(); + let status = out.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0); + (status, out.split_once("\r\n\r\n").map(|(_, b)| b.to_string()).unwrap_or_default()) +} + fn start(dir: &Path, port: u16) -> Child { let child = Command::new(env!("CARGO_BIN_EXE_wai-quantum-service")) .arg("serve") @@ -97,6 +122,178 @@ fn the_service_gives_every_receipt_the_verdict_the_corpus_gives_it() { assert!(wrong.is_empty(), "{} wrong:\n{}", wrong.len(), wrong.join("\n")); } +/// Text that does not read as a job's, a body that is no job, and a result with no receipt +/// each get a verdict with that reason alone (§8.4), as every surface gives it, not a refusal +/// of the request. As a tool, given the job's members, the host's unsealed ones among them, a +/// wasmQ job's unsealed status is noted where it differs from what is sealed. +#[test] +fn every_body_gets_a_verdict_and_the_tool_reads_the_body_it_is_given() { + let dir = std::env::temp_dir().join(format!("wai-qsvc-verdicts-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let port = free_port(); + let mut child = start(&dir, port); + let job = std::fs::read(corpus().join("jobs/cli-wasmq-fault.json")).unwrap(); + let mut bom = vec![0xef, 0xbb, 0xbf]; + bom.extend_from_slice(&job); + let mut problems = Vec::new(); + for (what, body, want) in [ + ("a byte order mark", bom, "unreadable"), + ("not JSON", b"not json".to_vec(), "unreadable"), + ("not UTF-8", vec![b'{', 0xff, b'}'], "unreadable"), + ("no job", br#"{"request": {}}"#.to_vec(), "not_a_job"), + ("no receipt", br#"{"request": {}, "result": {"counts": {}}}"#.to_vec(), "no_receipt"), + ] { + let (code, answer) = http(port, "POST", "/v1/verify", &body); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + if code != 200 || said["reasons"] != json!([want]) || said["valid"] != false { + problems.push(format!("{what}: {code} {answer}")); + } + } + let parsed: Value = serde_json::from_slice(&job).unwrap(); + let args = json!({ "status": "done", "method": parsed["method"], "request": parsed["request"], "result": parsed["result"] }); + let call = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "verify_quantum_receipt", "arguments": args } }); + let (_, answer) = http(port, "POST", "/mcp", call.to_string().as_bytes()); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + let s = &said["result"]["structuredContent"]; + if s["valid"] != true || s["wasmq"]["notes"] != json!([r#"the host's unsealed status says "done"; the sealed result says "fault""#]) { + problems.push(format!("the tool, given the body: {s}")); + } + let _ = child.kill(); + let _ = child.wait(); + let _ = std::fs::remove_dir_all(&dir); + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + +/// One answer to every form of a job (the provider protocol's section 8.4): an estimate or +/// an algorithm's answer with no receipt, or that is no job, is that reason alone over HTTP +/// and from the tool, given its text, the job as an object or its members; a forged +/// estimate sealed again is run again and is not valid however it is given; a genuine job +/// is valid given as an object; a job past 1 MiB is read, and one past what verify reads is +/// `too_large`, a verdict like any other. +#[test] +fn one_answer_to_every_form_of_a_job() { + let dir = std::env::temp_dir().join(format!("wai-qsvc-one-answer-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let port = free_port(); + let mut child = start(&dir, port); + let estimate = wai_quantum::engine::estimate::FORMAT; + let algorithms = wai_quantum::engine::schema::ALGORITHMS; + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[9; 32]); + let angles = "OPENQASM 3.0;\nqubit[4] q;\nh q;\nrz(0.3) q[0];\nccx q[0], q[1], q[2];\nry(1.1) q[1];\nt q[2];\nctrl(3) @ x q[0], q[1], q[2], q[3];\n"; + let made = wai_quantum::engine::estimate::sealed(&json!({ "qasm": angles, "budget": 1e-3, "max_slowdown": 64 }), &key).unwrap(); + let mut forged = made.clone(); + forged["result"]["estimate"]["physical_qubits"] = json!(1); + forged["result"].as_object_mut().unwrap().remove("receipt"); + wai_quantum::engine::receipt::seal_into(&key, &forged["request"].clone(), &mut forged["result"]); + let tool = |args: Value| -> Value { + let call = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "verify_quantum_receipt", "arguments": args } }); + let (_, answer) = http(port, "POST", "/mcp", call.to_string().as_bytes()); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + said["result"]["structuredContent"].clone() + }; + let mut problems = Vec::new(); + let mut each = |what: &str, body: &Value, ok: &dyn Fn(&Value) -> bool| { + let (code, answer) = http(port, "POST", "/v1/verify", body.to_string().as_bytes()); + let over_http: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + let mut members = body.as_object().cloned().unwrap_or_default(); + members.remove("job"); + for (form, said) in [("POST /v1/verify", if code == 200 { over_http } else { json!({ "status": code, "body": answer }) }), ("the tool, its text", tool(json!({ "job": body.to_string() }))), ("the tool, the job as an object", tool(json!({ "job": body }))), ("the tool, its members", tool(Value::Object(members)))] { + if !ok(&said) { + problems.push(format!("{what}, {form}: {said}")); + } + } + }; + let alone = |want: &'static str| move |v: &Value| v["valid"] == false && v["reasons"] == json!([want]); + each("an estimate with no receipt", &json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\nh q;\n" }, "result": { "format": estimate } }), &alone("no_receipt")); + each("an estimate with no receipt and no program", &json!({ "request": {}, "result": { "format": estimate } }), &alone("no_receipt")); + each("an estimate that is no job", &json!({ "request": 5, "result": { "format": estimate } }), &alone("not_a_job")); + each("an algorithm's answer with no receipt", &json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\n" }, "result": { "schema": algorithms, "call": "gradient" } }), &alone("no_receipt")); + each("an algorithm's answer that is no job", &json!({ "request": 5, "result": { "schema": algorithms } }), &alone("not_a_job")); + each("a genuine estimate", &made, &|v: &Value| v["valid"] == true && v["reproduced"] == true); + each("a forged estimate, sealed again", &forged, &|v: &Value| v["valid"] == false && v["reproduced"] == false && v["signed"] == true); + let job: Value = serde_json::from_slice(&std::fs::read(corpus().join("jobs/cli-wasmq-run.json")).unwrap()).unwrap(); + each("a genuine wasmQ job", &job, &|v: &Value| v["valid"] == true); + // A job of 1.5 MiB (a member nothing seals holds the rest) is read and judged. + let mut big = job.clone(); + big["pad"] = json!("x".repeat(3 << 19)); + each("a genuine job of 1.5 MiB", &big, &|v: &Value| v["valid"] == true); + // A request no host runs again, sealed: an estimate's verdict says so, as an algorithm's + // answer's does. + let reseal = |mut body: Value| { + body["result"].as_object_mut().unwrap().remove("receipt"); + wai_quantum::engine::receipt::seal_into(&key, &body["request"].clone(), &mut body["result"]); + body + }; + let unrunnable = reseal(json!({ "request": {}, "result": made["result"].clone() })); + each("an estimate whose request no estimate runs", &unrunnable, &|v: &Value| v["valid"] == false && v["reproduced"] == false && v["rerun_error"].is_string()); + // An algorithm's answer whose program nests past the reader's bound (a flat sum of 2000 + // terms) gets its verdict, and the service stays up. + let gradient = json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "params": { "t": 0.4 }, "hamiltonian": ["Z0"] }); + let answered = wai_quantum::engine::variational::seal(&gradient, wai_quantum::engine::variational::call("gradient", &gradient).unwrap(), &key); + each("a genuine gradient", &answered, &|v: &Value| v["valid"] == true && v["rerun"] == true); + let mut deep = answered.clone(); + deep["request"]["qasm"] = json!(format!("OPENQASM 3.0;\ninput float t;\nqubit q;\nint x = 1{};\nrx(t) q;\n", "+1".repeat(1999))); + each("a gradient of a program past the nesting bound", &reseal(deep), &|v: &Value| v["valid"] == false && v["rerun"] == false && v["rerun_error"].as_str().is_some_and(|e| e.contains("Q130"))); + let up = http(port, "GET", "/v1/health", b"").0 == 200; + // Past what verify reads: too_large, with what it reads, on every surface. Past the bytes: + let at_most = json!({ "bytes": 4 << 20, "values": 1 << 19 }); + let too_large = |v: &Value| v["valid"] == false && v["reasons"] == json!(["too_large"]) && v["read_at_most"] == json!({ "bytes": 4 << 20, "values": 1 << 19 }); + let padded = |n: usize| { + let mut j = job.clone(); + j["pad"] = json!(""); + let rest = j.to_string().len(); + j["pad"] = json!("x".repeat(n - rest)); + j + }; + each("a job of 4 MiB", &padded(4 << 20), &|v: &Value| v["valid"] == true); + each("a job of 4 MiB and a byte", &padded((4 << 20) + 1), &too_large); + // a job of quotes, 3 MiB of text, which a string of it doubles again, within them: + let mut quoted = job.clone(); + quoted["pad"] = json!("\"".repeat(3 << 19)); + each("a job of 3 MiB of escaped quotes", "ed, &|v: &Value| v["valid"] == true); + // past the values, in a few bytes each: + let mut many = job.clone(); + many["pad"] = json!(vec![0; 1 << 19]); + each("a job of 2^19 values more", &many, &too_large); + if !up { + problems.push("the service did not stay up".into()); + } + // and past what /mcp reads of a message: its verdict by its id, too_large. + let mut huge = job.clone(); + huge["pad"] = json!("x".repeat(30 << 20)); + let (code, answer) = http_past_its_limit(port, "/v1/verify", huge.to_string().into_bytes()); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + if code != 200 || said["reasons"] != json!(["too_large"]) || said["read_at_most"] != at_most { + problems.push(format!("a job of 30 MiB: {code} {}", &answer[..answer.len().min(300)])); + } + let call = json!({ "jsonrpc": "2.0", "id": 77, "method": "tools/call", "params": { "name": "verify_quantum_receipt", "arguments": { "job": huge } } }); + let (code, answer) = http_past_its_limit(port, "/mcp", call.to_string().into_bytes()); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + if code != 200 || said["id"] != 77 || !too_large(&said["result"]["structuredContent"]) { + problems.push(format!("a call of 30 MiB: {code} {}", &answer[..answer.len().min(300)])); + } + // A job nested past what a reader takes, as an object or as members: unreadable, by its id. + for d in [127usize, 128, 140, 1000] { + let raw = format!("{{\"request\":{},\"result\":{},\"pad\":{}{}}}", job["request"], job["result"], "[".repeat(d - 1), "]".repeat(d - 1)); + for (form, args) in [("as an object", format!("{{\"job\":{raw}}}")), ("as members", raw.clone()), ("as text", json!({ "job": raw }).to_string())] { + let call = format!("{{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{{\"name\":\"verify_quantum_receipt\",\"arguments\":{args}}}}}"); + let (code, answer) = http(port, "POST", "/mcp", call.as_bytes()); + let said: Value = serde_json::from_str(&answer).unwrap_or(Value::Null); + let s = &said["result"]["structuredContent"]; + let want = if d <= 127 { s["valid"] == true } else { s["reasons"] == json!(["unreadable"]) }; + if code != 200 || said["id"] != 5 || !want { + problems.push(format!("a job {d} deep, {form}: {code} {}", &answer[..answer.len().min(300)])); + } + } + } + let _ = child.kill(); + let _ = child.wait(); + let _ = std::fs::remove_dir_all(&dir); + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + /// [`http`], or nothing when the service does not answer. fn try_http(port: u16, method: &str, path: &str, body: &[u8]) -> Option<(u16, String)> { let mut s = TcpStream::connect(("127.0.0.1", port)).ok()?; @@ -148,7 +345,7 @@ fn a_program_past_the_nesting_bound_is_refused_and_the_service_stays_up() { continue; } // Read at once, it is refused by its code; queued, the job fails by it. - if (200..300).contains(&code) && !answer.contains("Q130") && path != "/v1/jobs" { + if (200..300).contains(&code) && !answer.contains("Q130") && path != "/v1/jobs" && path != "/v1/verify" { wrong.push(format!("{what} at {path}: {code} without Q130: {}", answer.chars().take(300).collect::())); } } diff --git a/wai/wai-quantum-views/src/lib.rs b/wai/wai-quantum-views/src/lib.rs index 9c8f92b0c..e63ca7f08 100644 --- a/wai/wai-quantum-views/src/lib.rs +++ b/wai/wai-quantum-views/src/lib.rs @@ -35,11 +35,14 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// One request: `{"op": "verify", "job": {...}}` or `{"op": "version"}`. The answer is JSON; a request that cannot /// be answered is `{"error": "..."}`. pub fn call(request: &str) -> String { - let req: Value = match serde_json::from_str(request) { - Ok(v) => v, + // Read so that a job gets its verdict however deep it nests: past what a reader takes it + // is unreadable, without the request read whole. + let (req, judged) = match job::read_carrying(request.as_bytes(), &[&["job"]], None) { + Ok(read) => read, Err(e) => return json!({ "error": format!("the request is not JSON: {e}") }).to_string(), }; match req.get("op").and_then(Value::as_str) { + Some("verify") if judged.is_some() => judged.unwrap_or_default(), Some("verify") => verify(req.get("job").unwrap_or(&Value::Null)), Some("version") => json!({ "views": VERSION }), Some(op) => json!({ "error": format!("no operation {op}: verify or version") }), @@ -50,15 +53,14 @@ pub fn call(request: &str) -> String { /// Whether `job`'s receipt seals its request and its result and every rule it /// can show holds: `{"valid", "reasons", "key", "form", "signer"}` (the signer -/// as a `did:key`), or `{"error"}` for what is not a job with a receipt. +/// as a `did:key`); what is not a job with a receipt has that reason alone. pub fn verify(job: &Value) -> Value { // Every rule of the receipt (§8.4, and §8.6 for a set), without what the // engine checks of a set past its receipt (writing a sweep's programs out // again), which would carry the whole engine into the page. - match job::verify(job) { - Ok(_) => job::verdict_of(&job::check(job)), - Err(e) => json!({ "error": e }), - } + // A body that is no job, or whose result carries no receipt, has that reason alone + // (§8.4), as every surface gives it. + job::verdict_of(&job::check(job)) } thread_local! { @@ -143,7 +145,8 @@ mod tests { let mut claimed = job.clone(); claimed["result"]["verdict"]["level"] = json!("proof"); assert_eq!(verify(&claimed)["valid"], false); - assert!(verify(&json!({ "request": {} }))["error"].is_string()); + // No job: that reason alone, a verdict as on every surface. + assert_eq!(verify(&json!({ "request": {} }))["reasons"], json!(["not_a_job"])); } /// A compile's certificate is sealed in a job's form: the view checks it alike. @@ -158,6 +161,48 @@ mod tests { assert_eq!(verify(&loosened)["valid"], false); } + /// A wasmQ job, checked in the view as on every surface: held to its profile's form + /// (quantum-wasmq §6.5), the run stated beside the verdict. + #[test] + fn a_wasmq_job_is_held_to_its_form_in_the_view() { + let job = |name: &str| -> Value { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/receipts/jobs").join(name); + serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap() + }; + let v = through_the_exports(&json!({ "op": "verify", "job": job("cli-wasmq-run.json") }).to_string()); + assert_eq!((v["valid"].as_bool(), v["wasmq"]["outcome"].as_str()), (Some(true), Some("ok")), "{v}"); + let v = verify(&job("crafted-wasmq-counts-short.json")); + assert_eq!(v["reasons"], json!(["wasmq_form"]), "{v}"); + } + + /// A job as deep as a reader takes (127) gets its verdict through the request around it, + /// and a parsed one deeper than that is unreadable, as its text would be. + #[test] + fn a_job_as_deep_as_a_reader_takes_gets_its_verdict() { + let corpus = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/receipts/jobs"); + let text = std::fs::read_to_string(corpus.join("crafted-read-depth-127.json")).unwrap(); + let v = through_the_exports(&format!("{{\"op\": \"verify\", \"job\": {text}}}")); + assert_eq!((v["valid"].as_bool(), v["reasons"].clone()), (Some(true), json!([])), "{v}"); + let mut deep = json!(0); + for _ in 0..128 { + deep = json!([deep]); + } + let mut job: Value = serde_json::from_str(&text).unwrap(); + job["note"] = deep; + assert_eq!(verify(&job)["reasons"], json!(["unreadable"])); + // Through the request, however deep: its verdict, not an error. + let plain = std::fs::read_to_string(corpus.join("cli-run-statevector.json")).unwrap(); + for d in [128usize, 133, 135, 136, 200, 100_000] { + let open = plain.trim_end().strip_suffix('}').unwrap(); + let job = format!("{open}, \"note\": {}{}}}", "[".repeat(d - 1), "]".repeat(d - 1)); + let v = through_the_exports(&format!("{{\"op\": \"verify\", \"job\": {job}}}")); + assert_eq!(v["reasons"], json!(["unreadable"]), "a job {d} deep: {v}"); + } + // A request nested past its own room outside the job is not read. + let v = through_the_exports(&format!("{{\"op\": \"verify\", \"job\": 1, \"x\": {}{}}}", "[".repeat(200), "]".repeat(200))); + assert!(v["error"].as_str().is_some_and(|e| e.contains("nested deeper")), "{v}"); + } + #[test] fn a_request_that_does_not_read_is_an_error() { assert!(through_the_exports("{")["error"].as_str().unwrap().contains("not JSON")); diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index e4d98a843..ca21c07ec 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" # quantum-conformance/qasm3/, quantum-conformance/qec-text/, the answer # layer's conformance set and quantum-conformance/formats/) to the crate; # they run in the repository only. -exclude = ["tests/landscape_conformance.rs", "tests/landscape_devices.rs", "tests/fabric_expected.rs", "tests/qasm3_conformance.rs", "tests/qec_text_conformance.rs", "tests/answer_conformance.rs", "tests/formats_conformance.rs", "tests/formats_mutation.rs"] +exclude = ["tests/landscape_conformance.rs", "tests/landscape_devices.rs", "tests/fabric_expected.rs", "tests/qasm3_conformance.rs", "tests/qec_text_conformance.rs", "tests/answer_conformance.rs", "tests/formats_conformance.rs", "tests/formats_mutation.rs", "tests/wasmq_verdict.rs"] # docs.rs builds with DEFAULT features only, which would document 3 of 21 # modules and hide the entire operational + ML layers. Document everything. @@ -26,7 +26,7 @@ all-features = true # `quantum` feature binds every reconstruction to a BLAKE3 hash. blake3 = "1" # Canonical receipt JSON (sorted keys, byte-stable round-trip). -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { version = "1", features = ["preserve_order", "unbounded_depth"] } # The engine's request and plan types (feature quantum_engine). serde = { version = "1", features = ["derive"], optional = true } # Receipt signatures. Optional — only the sealing features need it. diff --git a/wai/wai-quantum/docs/qasm-diagnostics.md b/wai/wai-quantum/docs/qasm-diagnostics.md index 58b98c2bd..8fde6cf30 100644 --- a/wai/wai-quantum/docs/qasm-diagnostics.md +++ b/wai/wai-quantum/docs/qasm-diagnostics.md @@ -116,6 +116,8 @@ engine. A test holds this page to the code's table. | `Q320` | refused | A measurement whose result goes nowhere: `measure q;` while bit registers are declared (write `c = measure q;`). | | `Q321` | refused | Qubits measured into a different number of bits. | | `Q322` | lowered | A bit declared inside a block or a subroutine: a bit of its own, which the steps read and the results do not show. | +| `Q323` | refused | A program past a bound its reader holds it to as it reads it: qubits, bits, steps, the bits its calls of externs take and give, or the work of reading it (a reader of programs from others, as the hybrid profile reads them). | +| `Q324` | refused | A measurement with no target bit (`measure q;`) in a program that calls externs: its outcome is the bits it declares, so each measurement names the bit it writes (write `c = measure q;`). | | `Q400` | warned | The method applies no durations: the program's delays, boxes and stretches change nothing it computes, and the result says so. | | `Q401` | carried | `delay[d]` on qubits, carried in the program's timing. | | `Q402` | carried | `box` (with or without a duration), carried in the timing around the gates it holds. | diff --git a/wai/wai-quantum/docs/qasm-feature-matrix.md b/wai/wai-quantum/docs/qasm-feature-matrix.md index 8d73642ae..4986a9298 100644 --- a/wai/wai-quantum/docs/qasm-feature-matrix.md +++ b/wai/wai-quantum/docs/qasm-feature-matrix.md @@ -121,7 +121,7 @@ and in the browser build. |---|---|---| | `c = measure q;`, `c[0] = measure q[0];`, `bit c = measure q;` | supported | Q321 | | `measure q -> c;` | supported | Q321 | -| `measure q;` | supported where no bit register is declared | Q320 | +| `measure q;` | supported where no bit register is declared, and refused in a program that calls externs | Q320, Q324 | | Mid-circuit measurement, measurement again, a gate after it | lowered: the program runs as steps (counts by shot splitting, exact expectation values) | | | `reset q;` | supported: before a qubit's first gate it changes nothing; after it, the program runs as steps | | | `barrier`, `barrier q;` | carried in the timing | Q404 | diff --git a/wai/wai-quantum/schemas/algorithms.v1.json b/wai/wai-quantum/schemas/algorithms.v1.json index 6e3450ec0..97cae8a40 100644 --- a/wai/wai-quantum/schemas/algorithms.v1.json +++ b/wai/wai-quantum/schemas/algorithms.v1.json @@ -1432,6 +1432,13 @@ "valid": { "type": "boolean" }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The provider protocol's section 8.4 reasons: none, since an answer is run again only once its receipt and every rule hold; a receipt that fails is answered with its reasons alone." + }, "signed": { "type": "boolean" }, diff --git a/wai/wai-quantum/schemas/estimate.v1.json b/wai/wai-quantum/schemas/estimate.v1.json index 9218de03a..1be749d02 100644 --- a/wai/wai-quantum/schemas/estimate.v1.json +++ b/wai/wai-quantum/schemas/estimate.v1.json @@ -374,6 +374,13 @@ "valid": { "type": "boolean" }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The provider protocol's section 8.4 reasons: none, since an answer is run again only once its receipt and every rule hold; a receipt that fails is answered with its reasons alone." + }, "signed": { "type": "boolean" }, @@ -383,6 +390,13 @@ "reproduced": { "type": "boolean" }, + "rerun_error": { + "type": [ + "string", + "null" + ], + "description": "Why the request could not be run again, when no host runs it (a request an estimate refuses): the answer is then not valid, and nothing is reproduced. Null when it ran." + }, "key": { "type": [ "string", diff --git a/wai/wai-quantum/src/engine/estimate/mod.rs b/wai/wai-quantum/src/engine/estimate/mod.rs index 7f669d35f..8ad9b3556 100644 --- a/wai/wai-quantum/src/engine/estimate/mod.rs +++ b/wai/wai-quantum/src/engine/estimate/mod.rs @@ -691,7 +691,15 @@ pub fn verify(body: &Value) -> Result { if result.get("format").and_then(Value::as_str) != Some(FORMAT) { return Err(format!("not an estimate: its result's format is not {FORMAT}")); } - let ev = evaluate(request)?; + let ev = match evaluate(request) { + Ok(ev) => ev, + // A request no host runs again: nothing is reproduced, and the verdict says why, as an + // algorithm's answer whose call no host runs does. + Err(e) => { + let signer = key.as_str().and_then(super::job::did_of); + return Ok(json!({ "valid": false, "signed": signed, "receipts": false, "reproduced": false, "rerun_error": e, "key": key, "signer": signer, "estimates": 0 })); + } + }; let parse = |v: &Value| ResourceReceipt::from_json(&v.to_string()); let key_bytes = key.as_str().and_then(|k| (0..32).map(|i| u8::from_str_radix(k.get(2 * i..2 * i + 2)?, 16).ok()).collect::>>()); let mine = |r: &ResourceReceipt| key_bytes.as_deref() == Some(r.signer_pubkey.as_slice()) && r.verify(); @@ -737,6 +745,7 @@ pub fn verify(body: &Value) -> Result { "signed": signed, "receipts": receipts, "reproduced": reproduced, + "rerun_error": null, "key": key, "signer": signer, "estimates": ev.points.len(), @@ -856,6 +865,11 @@ mod tests { assert_eq!(c.epsilon_each, body["result"]["epsilon"]["each"].as_f64().unwrap()); // The same request, the same bytes (but for nothing: the seal is deterministic). assert_eq!(sealed(&req, &key()).unwrap(), body); + // Every surface's one answer (`job::answer_of`): a seal that holds is run again. + use super::super::job::{Answer, answer_of, answer_of_text, rerun}; + assert!(matches!(answer_of(&body), Answer::Rerun(_)) && matches!(answer_of_text(&body.to_string()), Answer::Rerun(_))); + let again = rerun(&body).unwrap(); + assert_eq!((again["valid"].clone(), again["reasons"].clone()), (json!(true), json!([]))); // A changed figure, a changed program or a swapped receipt fails. let mut forged = body.clone(); forged["result"]["estimate"]["physical_qubits"] = json!(1); diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index 8cf2fdb37..06f1e1958 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -80,13 +80,21 @@ pub fn verify(job: &Value) -> Result<(bool, Value), String> { /// What a surface given a parsed job answers: every rule it can show /// ([`check`], with `reasons`, `form` and what a provider's result says of -/// itself), and the signer as a `did:key`. +/// itself), and the signer as a `did:key`. A body that is no job, or whose +/// result carries no receipt, has that reason alone (§8.4), as every surface +/// gives it. pub fn verdict(job: &Value) -> Result { - if is_set(job) { - return Ok(of_a_set(job, &check(job))); + let c = check(job); + if is_set(job) && !alone(&c) { + return Ok(of_a_set(job, &c)); } - verify(job)?; - Ok(verdict_of(&check(job))) + Ok(verdict_of(&c)) +} + +/// Whether a check found one of the reasons §8.4 reports alone: the job was not read +/// past it. +fn alone(c: &Check) -> bool { + c.reasons.first().is_some_and(|r| [UNREADABLE, NOT_A_JOB, NO_RECEIPT].contains(r)) } /// A set's verdict: its reasons (§8.6), and beside them what the engine @@ -103,15 +111,20 @@ fn of_a_set(job: &Value, c: &Check) -> Value { v } -/// [`verdict`] of a job given as text, read as §8.2 reads it ([`check_text`]). +/// [`verdict`] of a job given as text, read as §8.2 reads it ([`check_text`]): +/// text that does not read is `unreadable`, a verdict like any other. +/// The verdict of a job already checked as `c`: a set's says what the engine checks of it past +/// its receipt (§8.6). +pub fn verdict_of_checked(job: &Value, c: &Check) -> Value { + if !alone(c) && is_set(job) { of_a_set(job, c) } else { verdict_of(c) } +} + pub fn verdict_of_text(text: &str) -> Result { - let job: Value = serde_json::from_str(text).map_err(|e| format!("not JSON: {e}"))?; - if is_set(&job) { - let c = check_text(text); - return Ok(if c.reasons.first() == Some(&UNREADABLE) { verdict_of(&c) } else { of_a_set(&job, &c) }); + let (c, job) = checked_text(text); + match job { + Some(job) if !alone(&c) && is_set(&job) => Ok(of_a_set(&job, &c)), + _ => Ok(verdict_of(&c)), } - verify(&job)?; - Ok(verdict_of(&check_text(text))) } /// A check as a surface gives it. @@ -129,6 +142,9 @@ pub const UNREADABLE: &str = "unreadable"; pub const NOT_A_JOB: &str = "not_a_job"; /// The result carries no `receipt`. pub const NO_RECEIPT: &str = "no_receipt"; +/// The job's text is longer than the surface reads, which it states (§8.4): it read +/// none of it. +pub const TOO_LARGE: &str = "too_large"; /// A set's items (§8.6) are not of their form, not counted by its `count`, or /// not the items its request gives. pub const SET_ITEMS: &str = "set_items"; @@ -144,6 +160,10 @@ pub const SET_ROOT: &str = "set_root"; /// seals every number as its double; a first-form receipt seals a whole number /// from −2^63 to 2^64 − 1 by its digits, and every other by its double. pub const INEXACT_NUMBER: &str = "inexact_number"; +/// A wasmQ job (its sealed `format` says it is one) that the profile does not +/// write: a rule of wai/extensions/quantum-wasmq.md §6.5 fails. [`Check::wasmq`] +/// says which. +pub const WASMQ_FORM: &str = "wasmq_form"; /// What a verifier finds of a job body (wai/extensions/quantum-providers.md /// §8.4): every reason that applies, in the order of the reasons' table. It is @@ -162,6 +182,11 @@ pub struct Check { /// to state it: the class, whether its fields are stated, its counts' /// source and how the device was asked to run the program. pub provider: Option, + /// For a wasmQ job, beside the verdict (wai/extensions/quantum-wasmq.md §6.5): + /// `outcome`, how the run ended by what is sealed; `problems`, each rule the + /// job breaks; and `notes`, where the host's unsealed words differ from what + /// is sealed, which change no verdict. + pub wasmq: Option, } impl Check { @@ -169,8 +194,8 @@ impl Check { self.reasons.is_empty() } - /// The check as JSON: `valid`, `reasons` (sorted), `key`, `form`, and - /// `provider` when the result is a provider's. + /// The check as JSON: `valid`, `reasons` (sorted), `key`, `form`, `provider` + /// when the result is a provider's, and `wasmq` when it is a wasmQ job's. pub fn to_json(&self) -> Value { let mut reasons = self.reasons.clone(); reasons.sort_unstable(); @@ -178,6 +203,9 @@ impl Check { if let Some(p) = &self.provider { v["provider"] = p.clone(); } + if let Some(w) = &self.wasmq { + v["wasmq"] = w.clone(); + } v } @@ -192,6 +220,309 @@ impl Check { } } +/// How deep `v` nests arrays and objects, the outermost counted as the first, as §8.2 +/// counts a text's: walked without recursion, however deep it is. +fn nesting(v: &Value) -> usize { + // One iterator for each level open: what this holds follows the depth, not the width. + let mut stack: Vec + '_>> = vec![Box::new(std::iter::once(v))]; + let mut most = 0; + while let Some(top) = stack.last_mut() { + match top.next() { + Some(Value::Array(a)) => { + most = most.max(stack.len()); + stack.push(Box::new(a.iter())); + } + Some(Value::Object(o)) => { + most = most.max(stack.len()); + stack.push(Box::new(o.values())); + } + Some(_) => {} + None => { + stack.pop(); + } + } + } + most +} + +/// The most a message around a job adds to its nesting: a view's request (`{"op", "job"}`), +/// a JSON-RPC call's `params.arguments.job`. +pub const MESSAGE_NESTING: usize = 8; + +/// A message that carries a job (a view's request, a JSON-RPC call), read so that a job as +/// deep as §8.2 reads one still reaches its verdict: nested at most 127 and +/// [`MESSAGE_NESTING`] more, counted before anything is built. +pub fn read_message(bytes: &[u8]) -> Result { + let most = crate::ijson::MAX_DEPTH + MESSAGE_NESTING; + let (mut depth, mut in_string, mut escaped) = (0usize, false, false); + for &b in bytes { + if in_string { + match b { + _ if escaped => escaped = false, + b'\\' => escaped = true, + b'"' => in_string = false, + _ => {} + } + continue; + } + match b { + b'"' => in_string = true, + b'{' | b'[' => { + depth += 1; + if depth > most { + return Err(format!("nested deeper than {most}")); + } + } + b'}' | b']' => depth = depth.saturating_sub(1), + _ => {} + } + } + let mut de = serde_json::Deserializer::from_slice(bytes); + de.disable_recursion_limit(); + let mut values = de.into_iter::(); + let v = values.next().ok_or("no JSON value")?.map_err(|e| e.to_string())?; + match values.next() { + None => Ok(v), + Some(Err(e)) => Err(e.to_string()), + Some(Ok(_)) => Err("more than one JSON value".into()), + } +} + +/// The most of a job a surface reads (§8.4 `too_large`): its bytes, and its values. A +/// check holds a job's values as a tree, so what it asks of memory follows them as much as +/// its bytes: a surface bounds both, and says both. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bound { + pub bytes: u64, + pub values: u64, +} + +/// How many values a JSON text holds, counted in one pass with nothing built: one, and one +/// more for each `,`, `:`, `[` and `{` outside its strings (an object's member is its name +/// and its value). Any text counts, JSON or not. +pub fn values_in(bytes: &[u8]) -> u64 { + let (mut n, mut in_string, mut escaped) = (1u64, false, false); + for &b in bytes { + if in_string { + match b { + _ if escaped => escaped = false, + b'\\' => escaped = true, + b'"' => in_string = false, + _ => {} + } + continue; + } + match b { + b'"' => in_string = true, + b',' | b':' | b'[' | b'{' => n += 1, + _ => {} + } + } + n +} + +/// Whether a job's text is within `bound`: its bytes, then its values. +pub fn within(bytes: &[u8], bound: Bound) -> bool { + bytes.len() as u64 <= bound.bytes && values_in(bytes) <= bound.values +} + +/// What a surface that reads at most `bound` of a job finds of a longer one: `too_large`, +/// reported alone (§8.4), and what it reads (`read_at_most`: `bytes` and `values`). +pub fn too_large(bound: Bound) -> Value { + let mut v = Check::only(TOO_LARGE).to_json(); + v["read_at_most"] = json!({ "bytes": bound.bytes, "values": bound.values }); + v +} + +/// Where a value is in a message's text, found by [`scan`]: its first byte and the byte +/// after its last, the values it holds ([`values_in`] of its text), how deep it nests (0 for +/// a string, a number or a literal; 1 for an array or an object of them), and whether it is +/// a string. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Place { + pub start: usize, + pub end: usize, + pub values: u64, + pub depth: usize, + pub string: bool, +} + +/// A message's text, scanned once with nothing built and nothing kept but its first few +/// levels: where the values at `paths` are (each path a list of members from the top, +/// `["params", "arguments", "job"]`; the last of a name given twice), and how deep the text +/// nests outside them. A text cut short is scanned as far as it goes: a value it cuts has no +/// place. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Scan { + pub places: Vec>, + pub outside: usize, +} + +/// See [`Scan`]. +pub fn scan(bytes: &[u8], paths: &[&[&str]]) -> Scan { + struct Frame { + object: bool, + key: Option, + want_key: bool, + } + struct Open { + path: usize, + start: usize, + depth: usize, + values: u64, + deepest: usize, + literal: bool, + string: bool, + } + fn close(open: &mut Vec, out: &mut Scan, end: usize, values: u64) { + let o = open.pop().expect("an open place"); + out.places[o.path] = Some(Place { start: o.start, end, values: 1 + values - o.values, depth: o.deepest, string: o.string }); + } + let keep = paths.iter().map(|p| p.len()).max().unwrap_or(0); + // A value begins at `at`, at the current depth: is it at one of the paths? + let begins = |frames: &[Frame], depth: usize, open: &mut Vec, at: usize, values: u64, literal: bool, string: bool| { + if depth > keep || frames.len() != depth || frames.iter().any(|f| !f.object || f.key.is_none()) { + return; + } + if let Some(path) = paths.iter().position(|p| p.len() == depth && p.iter().zip(frames).all(|(k, f)| f.key.as_deref() == Some(*k))) { + open.push(Open { path, start: at, depth, values, deepest: 0, literal, string }); + } + }; + let mut out = Scan { places: vec![None; paths.len()], outside: 0 }; + let (mut frames, mut open): (Vec, Vec) = (Vec::new(), Vec::new()); + let (mut depth, mut values) = (0usize, 1u64); + let (mut in_string, mut escaped, mut key_string, mut string_start, mut in_literal) = (false, false, false, 0usize, false); + for (i, &b) in bytes.iter().enumerate() { + if in_string { + match b { + _ if escaped => escaped = false, + b'\\' => escaped = true, + b'"' => { + in_string = false; + if key_string { + if let Some(f) = frames.get_mut(depth.wrapping_sub(1)) { + let raw = &bytes[string_start..=i]; + f.key = Some(if raw.contains(&b'\\') { serde_json::from_slice::(raw).unwrap_or_default() } else { String::from_utf8_lossy(&raw[1..raw.len() - 1]).into_owned() }); + } + } else if open.last().is_some_and(|o| o.string && o.depth == depth) { + close(&mut open, &mut out, i + 1, values); + } + } + _ => {} + } + continue; + } + if in_literal && matches!(b, b',' | b':' | b']' | b'}' | b' ' | b'\t' | b'\n' | b'\r' | b'"' | b'[' | b'{') { + in_literal = false; + if open.last().is_some_and(|o| o.literal && o.depth == depth) { + close(&mut open, &mut out, i, values); + } + } + match b { + b'"' => { + key_string = depth >= 1 && depth <= keep && frames.get(depth - 1).is_some_and(|f| f.object && f.want_key); + if !key_string { + begins(&frames, depth, &mut open, i, values, false, true); + } + in_string = true; + string_start = i; + } + b'{' | b'[' => { + begins(&frames, depth, &mut open, i, values, false, false); + values += 1; + depth += 1; + if depth <= keep { + frames.push(Frame { object: b == b'{', key: None, want_key: b == b'{' }); + } + if open.is_empty() { + out.outside = out.outside.max(depth); + } + for o in open.iter_mut() { + o.deepest = o.deepest.max(depth - o.depth); + } + } + b'}' | b']' => { + depth = depth.saturating_sub(1); + frames.truncate(depth.min(keep)); + if open.last().is_some_and(|o| !o.literal && !o.string && o.depth == depth) { + close(&mut open, &mut out, i + 1, values); + } + } + b':' => { + values += 1; + if depth <= keep + && let Some(f) = frames.get_mut(depth.wrapping_sub(1)) + { + f.want_key = false; + } + } + b',' => { + values += 1; + if depth <= keep + && let Some(f) = frames.get_mut(depth.wrapping_sub(1)) + { + f.want_key = f.object; + f.key = None; + } + } + b' ' | b'\t' | b'\n' | b'\r' => {} + _ if !in_literal => { + in_literal = true; + begins(&frames, depth, &mut open, i, values, true, false); + } + _ => {} + } + } + if in_literal && open.last().is_some_and(|o| o.literal && o.depth == depth) { + close(&mut open, &mut out, bytes.len(), values); + } + out +} + +/// A message that carries a job, read so that the job gets its verdict whatever it is: the +/// message; and when the job at the first of `paths` the message holds is past `bound`, or +/// nests past what §8.2 reads (127), the message with `null` in its place beside the verdict +/// it gets (`too_large`, `unreadable`). A job given as a string is its text, and its bytes +/// and values are the text's; one given as JSON is as written there. Err when the message +/// does not read, or nests past [`MESSAGE_NESTING`] outside the job. +pub fn read_carrying(bytes: &[u8], paths: &[&[&str]], bound: Option) -> Result<(Value, Option), String> { + let found = scan(bytes, paths); + let most = crate::ijson::MAX_DEPTH + MESSAGE_NESTING; + if found.outside > most { + return Err(format!("nested deeper than {most}")); + } + let Some(place) = found.places.iter().flatten().next().copied() else { return read_message(bytes).map(|v| (v, None)) }; + let span = &bytes[place.start..place.end]; + let past = |text: &[u8]| bound.filter(|b| !within(text, *b)).map(too_large); + let verdict = if place.string { + // Its text: how long, and how many values, once its escapes are read. + serde_json::from_slice::(span).ok().and_then(|text| past(text.as_bytes())) + } else { + past(span).or_else(|| (place.depth > crate::ijson::MAX_DEPTH).then(|| verdict_of(&Check::only(UNREADABLE)))) + }; + let Some(verdict) = verdict else { return read_message(bytes).map(|v| (v, None)) }; + let mut rest = Vec::with_capacity(bytes.len() - span.len() + 4); + rest.extend_from_slice(&bytes[..place.start]); + rest.extend_from_slice(b"null"); + rest.extend_from_slice(&bytes[place.end..]); + read_message(&rest).map(|v| (v, Some(verdict))) +} + +/// What a JSON-RPC message's text says of itself before its end: its `id`, `method` and +/// `params.name`, each when the text holds it whole. A surface that reads no further than +/// it must answers a longer message with its id. +pub fn message_head(bytes: &[u8]) -> (Option, Option, Option) { + let found = scan(bytes, &[&["id"], &["method"], &["params", "name"]]); + let at = |k: usize| found.places[k].and_then(|p| serde_json::from_slice::(&bytes[p.start..p.end]).ok()); + (at(0), at(1).and_then(|v| v.as_str().map(str::to_string)), at(2).and_then(|v| v.as_str().map(str::to_string))) +} + +/// Whether a check found one of the reasons §8.4 reports alone, so that a surface says the +/// job's form was not judged: `unreadable`, `not_a_job` or `no_receipt`. +pub fn reported_alone(c: &Check) -> bool { + alone(c) +} + /// [`check_text`] of a job body given as bytes: `unreadable` when they are not /// UTF-8. pub fn check_bytes(bytes: &[u8]) -> Check { @@ -202,27 +533,109 @@ pub fn check_bytes(bytes: &[u8]) -> Check { /// each number by its literal, so that a whole number a double cannot hold is /// found even where a general reader would round it. pub fn check_text(text: &str) -> Check { - let Ok(tree) = crate::ijson::parse_lenient(text.as_bytes()) else { return Check::only(UNREADABLE) }; - let Ok(v) = serde_json::from_str::(text) else { return Check::only(UNREADABLE) }; + checked_text(text).0 +} + +/// What the strict reading of a job's text finds that its parsed value no longer shows: +/// whether a number written in what its receipt seals is no double's (by either form's +/// reading), and whether a set's item writes one its own receipt did not seal. +struct Literals { + inexact_first: bool, + inexact: bool, + set_item: bool, +} + +/// [`check_text`], and the job parsed when it reads: the strict reading first, kept only +/// for what [`Literals`] says, so that the two readings are never held at once. A surface +/// that goes on with the job takes it from here rather than reading the text again. +pub fn checked_text(text: &str) -> (Check, Option) { + let literals = { + let Ok(tree) = crate::ijson::parse_lenient(text.as_bytes()) else { return (Check::only(UNREADABLE), None) }; + let set = tree.get("result").and_then(|r| r.get("format")).and_then(|f| f.as_str()) == Some(super::sets::FORMAT); + Literals { + inexact_first: inexact_literal(&tree, true), + inexact: inexact_literal(&tree, false), + // A set's items are jobs: one that writes a number its receipt did not seal is not valid. + set_item: set + && matches!(tree.get("result").and_then(|r| r.get("items")), Some(crate::ijson::Json::Array(items)) if items.iter().any(|it| { + // An item's own receipt's form; one that failed has none, and its + // leaf is the canonical form's digest, which seals the double. + let first = it.get("result").and_then(|r| r.get("receipt")).is_some_and(|r| r.get("canon").is_none()); + inexact_literal(it, first) + })), + } + }; + let Ok(v) = serde_json::from_str::(text) else { return (Check::only(UNREADABLE), None) }; let mut c = check(&v); if c.reasons.first() != Some(&NOT_A_JOB) && c.reasons.first() != Some(&NO_RECEIPT) { - if inexact_literal(&tree, c.form == Some("first")) { + if if c.form == Some("first") { literals.inexact_first } else { literals.inexact } { c.push(INEXACT_NUMBER); } - // A set's items are jobs: one that writes a number its receipt did not seal is not valid. - if is_set(&v) - && let Some(crate::ijson::Json::Array(items)) = tree.get("result").and_then(|r| r.get("items")) - && items.iter().any(|it| { - // An item's own receipt's form; one that failed has none, and its - // leaf is the canonical form's digest, which seals the double. - let first = it.get("result").and_then(|r| r.get("receipt")).is_some_and(|r| r.get("canon").is_none()); - inexact_literal(it, first) - }) - { + if is_set(&v) && literals.set_item { c.push(SET_ITEM); } } - c + (c, Some(v)) +} + +/// Whether a job's answer has a verifier of its own past its receipt: an estimate, re-run +/// (`estimate::verify`), or an algorithm's answer (`variational::verify`). Never a wasmQ +/// job, which [`check`] holds to its profile's form whatever else its result says it is, +/// so that every surface routes a job alike. +pub fn has_own_verifier(job: &Value) -> bool { + !super::wasmq::is_job(&job["result"]) && (job["result"]["format"] == super::estimate::FORMAT || job["result"]["schema"] == super::schema::ALGORITHMS) +} + +/// What a surface answers of a job: its verdict (§8.4), or, for an estimate or an +/// algorithm's answer whose receipt and every rule of §8.4 hold, the run again its own +/// verifier makes ([`rerun`]). Text that does not read, a body that is no job and a result +/// with no receipt get that reason alone first, whatever kind of job the body says it is, +/// and a job whose seal or rules fail gets those reasons: so every surface answers a job +/// alike, given its text or its members, and only a sound seal is run again. +#[derive(Debug)] +pub enum Answer { + /// The verdict, as [`verdict_of_text`] and [`verdict`] give it. + Verdict(Value), + /// A job for [`rerun`]: a surface that runs work within bounds runs it there. + Rerun(Value), +} + +/// [`Answer`] of a job's text, read as §8.2 reads it. +pub fn answer_of_text(text: &str) -> Answer { + match checked_text(text) { + (c, Some(job)) if !alone(&c) => answer_checked(job, &c), + (c, _) => Answer::Verdict(verdict_of(&c)), + } +} + +/// [`Answer`] of a job given parsed: what a parsed job shows (the provider protocol's §9). +pub fn answer_of(job: &Value) -> Answer { + let c = check(job); + if alone(&c) { + return Answer::Verdict(verdict_of(&c)); + } + answer_checked(job.clone(), &c) +} + +fn answer_checked(job: Value, c: &Check) -> Answer { + if is_set(&job) { + return Answer::Verdict(of_a_set(&job, c)); + } + if c.valid() && has_own_verifier(&job) { + return Answer::Rerun(job); + } + Answer::Verdict(verdict_of(c)) +} + +/// An estimate's or an algorithm's answer, run again by its own verifier +/// (`estimate::verify`, `variational::verify`): the receipt already holds, so its §8.4 +/// reasons are none, and the verdict says so beside what the run again found. +pub fn rerun(job: &Value) -> Result { + let mut v = if job["result"]["format"] == super::estimate::FORMAT { super::estimate::verify(job)? } else { super::variational::verify(job) }; + if v.get("reasons").is_none() { + v["reasons"] = json!([]); + } + Ok(v) } /// Whether a job is a set's (§8.6). @@ -291,6 +704,10 @@ fn whole_and_exact(literal: &str) -> bool { /// double cannot hold is found only past `u64` and `i64`; [`check_text`] finds /// every one. pub fn check(job: &Value) -> Check { + // Deeper than any text a reader takes (§8.2): no job's text could be this value. + if nesting(job) > crate::ijson::MAX_DEPTH { + return Check::only(UNREADABLE); + } let (Some(request), Some(result)) = (job.get("request").filter(|r| r.is_object()), job.get("result").filter(|r| r.is_object())) else { return Check::only(NOT_A_JOB); }; @@ -302,6 +719,13 @@ pub fn check(job: &Value) -> Check { }; let set = is_set(job); let mut c = if set { check_set(request, result, seal) } else { check_parts(request, result, seal) }; + if super::wasmq::is_job(result) { + let problems = super::wasmq::problems(request, result); + if !problems.is_empty() { + c.push(WASMQ_FORM); + } + c.wasmq = Some(json!({ "outcome": result["status"].as_str(), "problems": problems, "notes": super::wasmq::notes(job) })); + } // Read as doubles already, the numbers a first-form receipt sealed as doubles // are past what this can see: `check_text` finds them. let sealed_result = result.as_object().is_some_and(|o| o.iter().any(|(k, x)| k != "receipt" && !(set && k == "items") && receipt::inexact_double(x).is_some())); @@ -316,7 +740,7 @@ fn hex(b: &[u8]) -> String { } /// JSON equality, numbers by their value and members in any order. -fn same(a: &Value, b: &Value) -> bool { +pub(super) fn same(a: &Value, b: &Value) -> bool { match (a, b) { (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(), (Value::Array(x), Value::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q)), @@ -546,6 +970,48 @@ pub fn answer(request: &Value, outcome: Result) -> Result(text) { + assert_eq!(verdict(&parsed).unwrap_or_else(|e| panic!("{text}: {e}"))["reasons"], json!([want]), "{text}"); + } + } + } + + /// An estimate's or an algorithm's answer is answered as every job is until its seal + /// holds: with no receipt, as no job, or as text that does not read, that reason alone, + /// given its text or its members; and run again only once its receipt and rules hold. + #[test] + fn a_job_is_run_again_only_on_a_seal_that_holds() { + let estimate = super::super::estimate::FORMAT; + let algorithms = super::super::schema::ALGORITHMS; + for (body, want) in [ + (json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\nh q;\n" }, "result": { "format": estimate } }), NO_RECEIPT), + (json!({ "request": {}, "result": { "format": estimate } }), NO_RECEIPT), + (json!({ "request": 5, "result": { "format": estimate } }), NOT_A_JOB), + (json!({ "result": { "format": estimate } }), NOT_A_JOB), + (json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\n" }, "result": { "schema": algorithms, "call": "gradient" } }), NO_RECEIPT), + (json!({ "request": 5, "result": { "schema": algorithms } }), NOT_A_JOB), + ] { + for (form, answer) in [("members", answer_of(&body)), ("text", answer_of_text(&body.to_string()))] { + match answer { + Answer::Verdict(v) => assert_eq!((v["valid"].as_bool(), v["reasons"].clone()), (Some(false), json!([want])), "{form}: {body}"), + Answer::Rerun(_) => panic!("{form}: {body} is run again"), + } + } + } + let twice = format!(r#"{{"request":{{}},"result":{{"format":"{estimate}","format":"{estimate}","receipt":{{}}}}}}"#); + assert!(matches!(answer_of_text(&twice), Answer::Verdict(v) if v["reasons"] == json!([UNREADABLE]))); + // A seal that does not hold is its reasons, not a run again. + let forged = json!({ "request": { "qasm": "OPENQASM 3.0;\nqubit q;\n" }, "result": { "format": estimate, "receipt": { "alg": "ed25519", "canon": "rfc8785", "key": "00".repeat(32), "digest": "00".repeat(32), "signature": "00".repeat(64) } } }); + assert!(matches!(answer_of(&forged), Answer::Verdict(v) if v["valid"] == false && v["reasons"].as_array().is_some_and(|r| !r.is_empty()))); + } + #[test] fn a_job_is_the_services_form_and_its_receipt_holds_only_as_given() { let req = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] }); diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 0338ed620..308da357c 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -255,4 +255,6 @@ pub mod variational; pub mod versions; #[cfg(feature = "quantum_receipt")] pub mod views; +#[cfg(feature = "quantum_receipt")] +pub mod wasmq; pub mod visual; diff --git a/wai/wai-quantum/src/engine/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs new file mode 100644 index 000000000..a7d18afd4 --- /dev/null +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -0,0 +1,1221 @@ +//! A wasmQ job's form (wai/extensions/quantum-wasmq.md §6.4, §6.5): what a verifier holds +//! a job of that profile to, with no envelope and no engine to run it. It is here, and not +//! in `wai-wasmq` (which depends on this crate), so that [`super::job::check`] holds every +//! wasmQ job to it, and every surface that checks a job gives one verdict: the command +//! line, the job service, the MCP servers, the Python package, the desktop app, a view and +//! a page. Only what is sealed decides it. A signature shows that a key sealed a job and +//! says nothing of what the job holds, so without the envelope this is what holds a result +//! to its contract and to itself. + +use super::job::same; +use serde_json::{Value, json}; +use std::collections::BTreeSet; + +/// The result's format: what makes a job a wasmQ job. +pub const FORMAT: &str = "wai.quantum.wasmq.result/1"; +/// The profile's name. +pub const PROFILE: &str = "wai.quantum.wasmq/1"; +/// The one method of this version of the profile. +pub const METHOD: &str = "stabilizer"; +/// The members a result always holds (§6.4); a fault adds `fault`. +pub const RESULT: [&str; 16] = ["format", "profile", "status", "method", "qubits", "shots", "shots_run", "seed", "counts", "callouts", "fuel", "modules", "transcript_blake3", "accuracy", "energy", "receipt"]; +/// A run's energy: the runner reads no meter, so its one claim is that it does not. +pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; +/// The members of a job's body (§6.4). +pub const BODY: [&str; 4] = ["status", "method", "request", "result"]; +/// The reasons a run can stop for (§6.2). +pub const REASONS: [&str; 11] = [ + "unreachable", + "memory_out_of_bounds", + "table_out_of_bounds", + "indirect_call_null", + "indirect_call_type", + "integer_divide_by_zero", + "integer_overflow", + "invalid_conversion_to_integer", + "fuel_exhausted", + "stack_exhausted", + "bad_result", +]; +/// The most shots a run takes. +pub const MAX_SHOTS: u64 = 1 << 20; +/// The largest size a number in a contract or a result may have: every JSON reader holds it +/// exactly. +pub const MAX_EXACT: u64 = (1 << 53) - 1; +/// The most qubits a program may declare. +pub const MAX_QUBITS: u32 = 1 << 15; +/// The longest envelope. +pub const MAX_ENVELOPE_BYTES: usize = 1 << 27; +/// The longest program. +pub const MAX_PROGRAM_BYTES: usize = 1 << 20; +/// The most bytes of WebAssembly an envelope holds, among all its modules. +pub const MAX_CODE_BYTES: usize = 1 << 19; +/// The most a run may ask of its host, in the units of §4.8. +pub const MAX_HOST_WORK: u128 = 1 << 42; +/// What a unit of fuel asks of a host, in those units. +pub const FUEL_WORK: u128 = 8; +/// The most bits a program holds, its blocks' own among them, and any one value or array (§4.8). +pub const MAX_BITS: u32 = 1 << 20; +/// The most bits a run's results hold: its shots times the bits of a shot (§4.8). +pub const MAX_RESULT_BITS: u128 = 1 << 28; +/// The host's part of a call into a module, or of making an instance (§4.8). +pub const CALL_WORK: u128 = 1 << 12; +/// Each byte of a module, for each run: what an engine takes to compile or decode it (a +/// body of calls grows some 53 times prepared, and the native engine compiles each byte of +/// that in about 1.5 microseconds) (§4.8). +pub const CODE_WORK: u128 = 1 << 17; +/// The most fuel any one budget may give (§2.2, §4.8): what a run may be given, the most work +/// a run asks of its host over the work a unit of fuel asks, 2^39. +pub const MAX_BUDGET: u64 = (MAX_HOST_WORK / FUEL_WORK) as u64; +/// The most steps a program has, its loops unrolled (§4.8). +pub const MAX_STEPS: u128 = 1 << 21; +/// A manifest's bytes but the digits of its target's three numbers (§2.1). +pub const MANIFEST_BYTES: u128 = 192; +/// An envelope's magic and lengths: `WAI1` and two lengths, `WQM1` and one (§2.1, §2.2). +pub const ENVELOPE_FRAMING: u128 = 20; +/// The least fuel a `stack_exhausted` fault used: a frame weighs at most 576 + 4096 + 2^8 + +/// 2^14 (§3.2, §4.3), so the stack's weight passes 2^23 at a call in the 394th frame +/// entered at the earliest, and each frame entered was charged its entry and the run that +/// holds its call, 2 at least (§4.2). +pub const STACK_FAULT_FUEL: u64 = 2 * ((1 << 23) / (576 + 4096 + 256 + (1 << 14)) + 1); +/// What a transcript's digest starts with (§6.4). +pub const TRANSCRIPT_PREFIX: &[u8] = b"wai:wasmq-transcript\x01"; +/// The most problems a verdict lists; the count of the rest follows them. +pub const PROBLEMS_LISTED: usize = 64; +/// The longest a listed problem is: a job's own text is quoted in some. +const PROBLEM_BYTES: usize = 256; + +/// Whether a result is a wasmQ job's: its sealed `format` says so. +pub fn is_job(result: &Value) -> bool { + result["format"] == FORMAT +} + +/// The status a job's body states for a result: `done`, or `fault` when the run stopped. +pub fn body_status(result: &Value) -> &'static str { + if result["status"] == "ok" { "done" } else { "fault" } +} + +/// A number by its value, as a whole number from 0 to [`MAX_EXACT`], however it is written: +/// `1`, `1.0` and `1e0` are one number, sealed as one (the provider protocol's §8.2). +fn count(v: &Value) -> Option { + // A whole number past 2^53 - 1 reads as its double, which is past it too: no count, + // however exactly a reader holds its digits. + v.as_u64().filter(|&n| n <= MAX_EXACT).or_else(|| v.as_f64().filter(|x| x.fract() == 0.0 && (0.0..=MAX_EXACT as f64).contains(x)).map(|x| x as u64)) +} + +/// Whether `v` is a whole number from `lo` to `hi`, by its value. +fn whole(v: &Value, lo: u64, hi: u64) -> bool { + count(v).is_some_and(|n| (lo..=hi).contains(&n)) +} + +fn is_digest(v: &Value) -> bool { + v.as_str().is_some_and(|s| s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))) +} + +/// Holds `v` to exactly these members: a message for each it lacks, or holds besides. +fn members(rule: u8, v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mut Vec) { + let Some(o) = v.as_object() else { + out.push(format!("rule {rule}: {what} is not an object")); + return; + }; + for name in required { + if !o.contains_key(*name) { + out.push(format!("rule {rule}: {what} has no `{name}`")); + } + } + for name in o.keys() { + if !required.contains(&name.as_str()) && !optional.contains(&name.as_str()) { + out.push(format!("rule {rule}: {what} holds `{name}`, which the text does not define")); + } + } +} + +/// The first number in `v` past [`MAX_EXACT`] in size, by its path: readers of a receipt +/// would not all hold it. One path is grown and cut back as the walk goes, so the walk is +/// linear in what it reads, however long a member's name. +fn past_exact(v: &Value) -> Option { + fn walk(v: &Value, path: &mut String) -> bool { + match v { + Value::Number(n) => n.as_f64().is_some_and(|x| x.abs() > MAX_EXACT as f64), + Value::Array(a) => a.iter().enumerate().any(|(i, x)| { + let at = path.len(); + path.push_str(&format!("[{i}]")); + let hit = walk(x, path); + if !hit { + path.truncate(at); + } + hit + }), + Value::Object(m) => m.iter().any(|(k, x)| { + let at = path.len(); + path.push('.'); + path.push_str(k); + let hit = walk(x, path); + if !hit { + path.truncate(at); + } + hit + }), + _ => false, + } + } + let mut path = String::new(); + walk(v, &mut path).then(|| path.trim_start_matches('.').to_string()) +} + +/// Holds a contract to §2.2: exactly its members, each of its form, within the profile's ranges. +fn contract_form(c: &Value, out: &mut Vec) { + // Each problem names its rule first, then that it is the contract's. + let mut problem = |s: String| out.push(match s.split_once(": ") { + Some((rule, rest)) if rule.starts_with("rule ") => format!("{rule}: the contract: {rest}"), + _ => format!("the contract: {s}"), + }); + let mut sub = Vec::new(); + // Rule 3. + members(3, c, "it", &["profile", "program", "modules", "callouts", "run"], &[], &mut sub); + members(3, &c["program"], "its program", &["blake3", "bytes", "inputs"], &[], &mut sub); + members(3, &c["run"], "its run", &["shots", "seed", "method", "state", "fuel"], &[], &mut sub); + members(3, &c["run"]["fuel"], "its budgets", &["per_call", "per_job", "per_instance"], &[], &mut sub); + for s in sub { + problem(s); + } + // Rule 4. + if c["profile"] != PROFILE { + problem(format!("rule 4: its profile is {}, not {PROFILE}", c["profile"])); + } + // Rule 5. + let p = &c["program"]; + if !is_digest(&p["blake3"]) || !whole(&p["bytes"], 1, MAX_PROGRAM_BYTES as u64) { + problem(format!("rule 5: its program is {p}, not a digest and a length")); + } + if !p["inputs"].as_object().is_some_and(|m| m.values().all(|v| v.as_f64().is_some_and(|x| x.fract() == 0.0 && x.abs() <= MAX_EXACT as f64))) { + problem(format!("rule 5: its program's inputs are {}, not whole numbers", p["inputs"])); + } + // Rule 6. + let modules = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if modules.is_empty() { + problem("rule 6: it holds no module".into()); + } + let mut code = 0u64; + for m in modules { + let mut sub = Vec::new(); + members(6, m, "a module", &["name", "blake3", "bytes", "priors"], &[], &mut sub); + sub.into_iter().for_each(&mut problem); + if !m["name"].is_string() || !is_digest(&m["blake3"]) || !whole(&m["bytes"], 1, MAX_EXACT) { + problem(format!("rule 6: a module is {}, not a name, a digest and a length", json!({ "name": m["name"], "blake3": m["blake3"], "bytes": m["bytes"] }))); + } + code = code.saturating_add(count(&m["bytes"]).unwrap_or(0)); + let priors = m["priors"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if !m["priors"].is_array() { + problem(format!("rule 6: a module's priors are {}", m["priors"])); + } + for pr in priors { + let mut sub = Vec::new(); + members(6, pr, "a prior", &["name", "blake3", "bytes"], &[], &mut sub); + sub.into_iter().for_each(&mut problem); + if !pr["name"].is_string() || !is_digest(&pr["blake3"]) || !whole(&pr["bytes"], 0, MAX_EXACT) { + problem(format!("rule 6: a prior is {pr}, not a name, a digest and a length")); + } + } + } + if code > MAX_CODE_BYTES as u64 { + problem(format!("rule 6: its modules hold {code} bytes, past {MAX_CODE_BYTES}")); + } + // Rule 7. + let callouts = c["callouts"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if callouts.is_empty() { + problem("rule 7: it names no callout".into()); + } + let mut externs = BTreeSet::new(); + let mut named = BTreeSet::new(); + for x in callouts { + let mut sub = Vec::new(); + members(7, x, "a callout", &["extern", "module", "export"], &[], &mut sub); + sub.into_iter().for_each(&mut problem); + if !x["extern"].is_string() || !x["export"].is_string() || !whole(&x["module"], 0, modules.len().saturating_sub(1) as u64) || modules.is_empty() { + problem(format!("rule 7: a callout is {x}, not an extern, a module it holds and an export")); + } + if let Some(name) = x["extern"].as_str() + && !externs.insert(name) + { + problem(format!("rule 7: two callouts for the extern {name:?}")); + } + named.extend(count(&x["module"])); + } + for k in 0..modules.len() { + if !named.contains(&(k as u64)) { + problem(format!("rule 7: module {k} is named by no callout")); + } + } + // Rule 8. + let run = &c["run"]; + if !whole(&run["shots"], 1, MAX_SHOTS) { + problem(format!("rule 8: its shots are {}, where a run takes 1 to {MAX_SHOTS}", run["shots"])); + } + if !whole(&run["seed"], 0, MAX_EXACT) { + problem(format!("rule 8: its seed is {}", run["seed"])); + } + if run["method"] != METHOD { + problem(format!("rule 8: its method is {}, where this version of the profile has {METHOD}", run["method"])); + } + if run["state"] != "shot" && run["state"] != "job" { + problem(format!("rule 8: its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); + } + // Each budget a count, per_call and per_instance within what a run may be given (rule 30 + // holds per_job to less). + for (b, most) in [("per_call", MAX_BUDGET), ("per_job", MAX_EXACT), ("per_instance", MAX_BUDGET)] { + if !whole(&run["fuel"][b], 0, most) { + problem(format!("rule 8: its fuel {b} is {}, past what a run may be given", run["fuel"][b])); + } + } +} + +/// What makes a job other than one this profile writes (§6.5), from what is sealed: its +/// request, and its result with its receipt's form. Empty when it is one. +pub fn problems(req: &Value, r: &Value) -> Vec { + let mut out = Vec::new(); + // Rule 1. + // A number a double cannot hold would be read differently by readers of the receipt. + let mut sealed = r.clone(); + if let Some(o) = sealed.as_object_mut() { + o.shift_remove("receipt"); + } + for (what, value) in [("request", req), ("result", &sealed)] { + if let Some(at) = past_exact(value) { + out.push(format!("rule 1: the {what}'s `{at}` is past 2^53 - 1")); + } + } + // Rule 2. + members(2, req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); + if !is_digest(&req["envelope_blake3"]) { + out.push(format!("rule 2: the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); + } + // Rule 9. + members(9, r, "the result", &RESULT, &["fault"], &mut out); + let c = &req["wasmq"]; + let run = &c["run"]; + contract_form(c, &mut out); + // Rule 10. + // The seal the engine writes: a receipt of the first form is an earlier host's, and no + // host of this profile wrote one. + if r["receipt"]["canon"] != "rfc8785" { + out.push(format!("rule 10: the receipt's canon is {}, not rfc8785", r["receipt"]["canon"])); + } + // Rule 11. + // What the result repeats from the contract, or fixes. + let fixed = [("format", json!(FORMAT)), ("profile", json!(PROFILE)), ("method", run["method"].clone()), ("shots", run["shots"].clone()), ("seed", run["seed"].clone()), ("accuracy", json!({ "class": "exact" })), ("energy", json!({ "unmetered": UNMETERED }))]; + for (name, want) in fixed { + if !same(&r[name], &want) { + out.push(format!("rule 11: the result's {name} is {}, not {want}", r[name])); + } + } + // Rule 12. + if count(&r["qubits"]).is_none_or(|q| q == 0 || q > u64::from(MAX_QUBITS)) { + out.push(format!("rule 12: the result's qubits are {}", r["qubits"])); + } + if !is_digest(&r["transcript_blake3"]) { + out.push(format!("rule 12: the result's transcript_blake3 is {}, not a digest", r["transcript_blake3"])); + } + // Rule 13. + // The fuel: within its budgets, and what the calls and the instances add up to. + let fuel = &r["fuel"]; + members(13, fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); + if !same(&fuel["budget"], &run["fuel"]) { + out.push(format!("rule 13: the fuel's budget is {}, where the contract gives {}", fuel["budget"], run["fuel"])); + } + let per_job = count(&run["fuel"]["per_job"]).unwrap_or(0); + if count(&fuel["used"]).is_none_or(|u| u > per_job) { + out.push(format!("rule 13: fuel used {} is past the run's budget {per_job}", fuel["used"])); + } + // Rule 14. + let per_call = count(&run["fuel"]["per_call"]).unwrap_or(0); + let externs: BTreeSet<&str> = c["callouts"].as_array().into_iter().flatten().filter_map(|x| x["extern"].as_str()).collect(); + if !fuel["callouts"].is_object() { + out.push(format!("rule 14: the fuel of the callouts is {}, not an object", fuel["callouts"])); + } + let mut spent = count(&fuel["instances"]); + let mut calls = Some(0u64); + for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { + members(14, f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); + if !externs.contains(name.as_str()) { + out.push(format!("rule 14: fuel for {name}, a callout the contract does not name")); + } + let (n, all, max) = (count(&f["calls"]), count(&f["fuel"]), count(&f["max"])); + if max.is_none_or(|m| m > per_call) { + out.push(format!("rule 14: callout {name} used {} in one call, past the budget {per_call}", f["max"])); + } + if max.is_none_or(|m| all.is_none_or(|a| m > a)) { + out.push(format!("rule 14: callout {name}'s largest call, {}, is past its fuel, {}", f["max"], f["fuel"])); + } + if n.is_none_or(|n| n == 0) { + out.push(format!("rule 14: callout {name} has fuel for {} calls", f["calls"])); + } + // Its fuel against its calls and their largest is rule 20's. + if all.is_none() { + out.push(format!("rule 14: callout {name}'s fuel is {}", f["fuel"])); + } + spent = spent.zip(all).and_then(|(a, b)| a.checked_add(b)); + calls = calls.zip(n).and_then(|(a, b)| a.checked_add(b)); + } + // Rule 15. + if spent.is_none() || spent != count(&fuel["used"]) { + out.push(format!("rule 15: fuel used {}, where the instances and the calls add up to {spent:?}", fuel["used"])); + } + if calls.is_none() || calls != count(&r["callouts"]) { + out.push(format!("rule 15: {} callouts, where the fuel counts {calls:?}", r["callouts"])); + } + // Rule 16. + // The modules the contract pins. + let mods = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + let got = r["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if mods.len() != got.len() || mods.iter().zip(got).any(|(a, b)| !same(&a["blake3"], &b["blake3"]) || !same(&a["name"], &b["name"])) { + out.push("rule 16: the result's modules are not the ones the contract pins".into()); + } + for m in got { + members(16, m, "a module of the result", &["name", "blake3"], &[], &mut out); + if !is_digest(&m["blake3"]) { + out.push(format!("rule 16: a module of the result whose blake3 is {}, not a digest", m["blake3"])); + } + } + // Rule 17. + // What the instances could be given: for each instance made, its seed, its init and + // each prior, each under the instance budget (§4.2, §5.3). + let per_instance = count(&run["fuel"]["per_instance"]).unwrap_or(0); + let stretches: u128 = mods.iter().map(|m| 2 + m["priors"].as_array().map_or(0, Vec::len) as u128).sum(); + let made = if run["state"] == "job" { 1 } else { u128::from(count(&r["shots_run"]).unwrap_or(0)) + u128::from(r["status"] == "fault") }; + if count(&fuel["instances"]).is_none_or(|n| u128::from(n) > made.saturating_mul(stretches).saturating_mul(u128::from(per_instance))) { + out.push(format!("rule 17: the instances' fuel, {}, is past what {made} of them could be given", fuel["instances"])); + } + // Rule 18. + // How it ended. + let (shots, shots_run) = (count(&r["shots"]), count(&r["shots_run"])); + match r["status"].as_str() { + Some("ok") => { + if r.get("fault").is_some() { + out.push("rule 18: a result that is ok and names a fault".into()); + } + if shots_run.is_none() || shots_run != shots { + out.push(format!("rule 18: a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); + } + } + Some("fault") => { + let f = &r["fault"]; + members(18, f, "the fault", &["reason", "shot", "phase", "module", "fuel"], &["callout", "budget"], &mut out); + let reason = f["reason"].as_str(); + if !reason.is_some_and(|x| REASONS.contains(&x)) { + out.push(format!("rule 18: a fault the profile does not name: {}", f["reason"])); + } + if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) { + out.push(format!("rule 18: a fault after {} of {} shots", r["shots_run"], r["shots"])); + } + if shots_run.is_none() || count(&f["shot"]) != shots_run { + out.push(format!("rule 18: a fault in shot {}, after {} shots finished", f["shot"], r["shots_run"])); + } + let phase = f["phase"].as_str(); + if !phase.is_some_and(|p| ["seed", "prior", "init", "call"].contains(&p)) { + out.push(format!("rule 18: a fault in a phase the profile does not name: {}", f["phase"])); + } + if !mods.iter().any(|m| same(&m["name"], &f["module"])) { + out.push(format!("rule 18: a fault in {}, a module the contract does not name", f["module"])); + } + // A fault's fuel within F's used is rules 20's and 25's, and a call's within + // per_call its callout's max's. + let used = count(&f["fuel"]); + let is_call = phase == Some("call"); + if used.is_none_or(|n| !is_call && n > per_instance) { + out.push(format!("rule 18: a fault after {} fuel, past the {per_instance} an instance's phase is given", f["fuel"])); + } + match f.get("callout") { + Some(x) if !is_call || !x.as_str().is_some_and(|s| externs.contains(s)) => out.push(format!("rule 18: a fault that names the callout {x} in phase {}", f["phase"])), + None if is_call => out.push("rule 18: a call's fault that names no callout".into()), + _ => {} + } + match f.get("budget") { + Some(x) if reason != Some("fuel_exhausted") => out.push(format!("rule 18: a budget, {x}, on a fault of {}", f["reason"])), + Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("rule 18: a fuel fault of the budget {x} in phase {}", f["phase"])), + None if reason == Some("fuel_exhausted") => out.push("rule 18: a fuel fault that names no budget".into()), + _ => {} + } + // The call that stopped is one of the calls made (§6.4): its callout's fuel + // counts it, and the most one of its calls used is at least what it used. + if is_call && let Some(name) = f["callout"].as_str() { + match fuel["callouts"].get(name) { + None => out.push(format!("rule 18: a call of {name:?} that stopped, where the fuel counts no call of it")), + Some(m) if count(&m["max"]).zip(used).is_none_or(|(max, u)| u > max) => out.push(format!("rule 18: a call of {name:?} that stopped after {} fuel, past the most one of its calls used, {}", f["fuel"], m["max"])), + Some(_) => {} + } + } + } + _ => out.push(format!("rule 18: status {}", r["status"])), + } + // Rule 19. + // The counts: of the shots that finished, each outcome the program's bits. + let counts = r["counts"].as_object(); + if counts.is_none() { + out.push(format!("rule 19: the result's counts are {}", r["counts"])); + } + let mut total = Some(0u64); + let mut width = None; + for (outcome, n) in counts.into_iter().flatten() { + if !outcome.bytes().all(|b| b == b'0' || b == b'1') { + out.push(format!("rule 19: an outcome that is not bits: {outcome:?}")); + } + if *width.get_or_insert(outcome.len()) != outcome.len() { + out.push(format!("rule 19: outcomes of {} and {} bits", width.unwrap_or(0), outcome.len())); + } + if count(n).is_none_or(|n| n == 0) { + out.push(format!("rule 19: a count of {n} for {outcome:?}")); + } + total = total.zip(count(n)).and_then(|(a, b)| a.checked_add(b)); + } + if total.is_none() || total != shots_run { + out.push(format!("rule 19: counts sum to {total:?}, not the {} shots run", r["shots_run"])); + } + across(c, req.get("wasmq").is_some(), r, &mut out); + listed(out) +} + +/// Rules 20 to 35 (§6.5): what the writer guarantees across members, each a tie the +/// members' own rules leave open. The fuel a returned call or prior used; a fault's +/// module against its callout, its phase against the module's priors, its reason against +/// its phase, its fuel against the instances and its limit, its shot against the state and +/// the calls; and §4.8's sums and products, over as much of them as a receipt shows. +fn across(c: &Value, given: bool, r: &Value, out: &mut Vec) { + let run = &c["run"]; + let fuel = &r["fuel"]; + let budget = |name: &str| count(&run["fuel"][name]).unwrap_or(0); + let mods = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + fn priors(m: &Value) -> &[Value] { + m["priors"].as_array().map(Vec::as_slice).unwrap_or(&[]) + } + let bytes = |v: &Value| u128::from(count(v).unwrap_or(0)); + let fault = if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }; + let phase = fault.and_then(|f| f["phase"].as_str()); + let in_call = phase == Some("call"); + let lifecycle = fault.is_some_and(|f| f.get("phase").is_some()) && !in_call; + let stopped = if in_call { fault.and_then(|f| f["callout"].as_str()) } else { None }; + let fault_fuel = fault.and_then(|f| count(&f["fuel"])); + // 20: the call that stopped used the fault's fuel, and each call that returned from 1 + // (§4.2: entering a function weighs 1) to the most one call used; when the most is more + // than the stopped call's, it was a returned call's, and the others used 1 at least. + for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { + let mine = u64::from(stopped == Some(name.as_str())); + let e = mine * fault_fuel.unwrap_or(0); + let returned = count(&f["calls"]).map(|n| n.saturating_sub(mine)); + let theirs = count(&f["fuel"]).and_then(|all| all.checked_sub(e)); + let max = count(&f["max"]); + let ranged = returned.zip(theirs).zip(max).map(|((k, used), max)| (u128::from(k), u128::from(used), u128::from(max))); + if ranged.is_none_or(|(k, used, max)| used < k || used > k * max) { + out.push(format!("rule 20: callout {name}'s {} calls that returned used {}, where each uses 1 to its max, {}", returned.map_or(f["calls"].to_string(), |k| k.to_string()), theirs.map_or(format!("{} less the stopped call's", f["fuel"]), |t| t.to_string()), f["max"])); + } else if let Some((k, used, max)) = ranged + && max > u128::from(e) + && used < (max + k).saturating_sub(1) + { + out.push(format!("rule 20: callout {name}'s {k} calls that returned used {used}, less than its max, {max}, used by one of them, and 1 for each other")); + } + } + // 21: what making the instances took at least: each set made whole passed every prior + // (cabi_realloc, at least 2, its bytes, and wasmq-prior, at least 1) and called the + // export a lifecycle fault shows; the set a lifecycle fault stopped passed the priors of + // the modules before its own, and its own when it stopped in init (§4.2, §5.2, §5.3). + let prior_part = |m: &Value| priors(m).iter().map(|p| bytes(&p["bytes"]) + 3).fold(0, u128::saturating_add); + let sets = u128::from(count(&r["shots_run"]).unwrap_or(0)) + u128::from(in_call); + let sets = if run["state"] == "job" { sets.min(1) } else { sets }; + let shown = u128::from(lifecycle && matches!(phase, Some("seed" | "init"))); + let per_set = mods.iter().map(prior_part).fold(shown, u128::saturating_add); + // The modules the stopped set passed every prior of. + let stopped_set: &[Value] = match fault.filter(|_| lifecycle).and_then(|f| mods.iter().position(|m| same(&m["name"], &f["module"]))) { + Some(k) if phase == Some("init") => &mods[..=k], + Some(k) => &mods[..k], + None => &[], + }; + let partial = stopped_set.iter().map(prior_part).fold(0, u128::saturating_add); + let least = sets.saturating_mul(per_set).saturating_add(partial); + if count(&fuel["instances"]).is_none_or(|n| u128::from(n) < least) { + out.push(format!("rule 21: the instances' fuel, {}, is less than the {least} making {sets} whole sets of instances, and the part a fault stopped, took at least", fuel["instances"])); + } + if let Some(f) = fault { + // 22: a call's fault is in the module of the callout it names (§5.2, §6.2). + if in_call { + let mut callouts = c["callouts"].as_array().into_iter().flatten().filter(|x| x["extern"].is_string() && x["extern"] == f["callout"]).peekable(); + let named = |x: &Value| count(&x["module"]).and_then(|k| mods.get(usize::try_from(k).ok()?)).is_some_and(|m| same(&m["name"], &f["module"])); + if callouts.peek().is_none() || !callouts.all(named) { + out.push(format!("rule 22: a call's fault in {}, not the module of the callout {}", f["module"], f["callout"])); + } + } + // 23: a fault while a prior is passed is in a module that has one (§5.3). + if phase == Some("prior") && !mods.iter().any(|m| same(&m["name"], &f["module"]) && !priors(m).is_empty()) { + out.push(format!("rule 23: a fault while a prior is passed, in {}, which has none", f["module"])); + } + // 24: a result that is not one is a call's or a prior's (§5.2). + if f["reason"] == "bad_result" && phase != Some("call") && phase != Some("prior") { + out.push(format!("rule 24: a bad_result in phase {}, which passes and returns nothing", f["phase"])); + } + if lifecycle { + // 25: a lifecycle call's fuel is part of the instances', beside what rule 21 counts. + if fault_fuel.zip(count(&fuel["instances"])).is_none_or(|(n, all)| u128::from(n).saturating_add(least) > u128::from(all)) { + out.push(format!("rule 25: a fault after {} fuel outside a call, past the instances' {} less the {least} the calls before it took", f["fuel"], fuel["instances"])); + } + // 26: the one instance of a run whose state is job is made in shot 0 (§5.3). + if run["state"] == "job" && count(&f["shot"]) != Some(0) { + out.push(format!("rule 26: a fault making the run's one instance in shot {}, where it is made in shot 0", f["shot"])); + } + // 27: the first shot's instances are made before any call (§5.3). + if count(&f["shot"]) == Some(0) && count(&r["callouts"]) != Some(0) { + out.push(format!("rule 27: a fault making the first shot's instances, after {} callouts", r["callouts"])); + } + } + // 28: a fuel fault used its whole limit: its budget's, or what was left of the run's (§4.2). + if f["reason"] == "fuel_exhausted" { + // Its own limit: a call's or a lifecycle call's; a fault with no phase has none. + let own = if in_call { Some(budget("per_call")) } else if lifecycle { Some(budget("per_instance")) } else { None }; + match f["budget"].as_str() { + Some("call") if fault_fuel != Some(budget("per_call")) => out.push(format!("rule 28: a fuel fault after {} fuel, where the call's limit is {}", f["fuel"], budget("per_call"))), + Some("instance") if fault_fuel != Some(budget("per_instance")) => out.push(format!("rule 28: a fuel fault after {} fuel, where the instance's limit is {}", f["fuel"], budget("per_instance"))), + Some("run") if count(&fuel["used"]).is_none_or(|u| Some(u) != count(&run["fuel"]["per_job"])) || fault_fuel.is_none_or(|n| own.is_some_and(|own| n >= own)) => { + out.push(format!("rule 28: a fuel fault of the run's budget, after {} fuel in a stretch whose own limit is {}, with {} of {} used", f["fuel"], own.map_or("none".to_string(), |o| o.to_string()), fuel["used"], budget("per_job"))) + } + _ => {} + } + } + } + // 29: the bits of a shot are at most 2^20, and the shots' bits at most 2^28 (§4.8); a + // name's length in characters. + let shots = u128::from(count(&run["shots"]).unwrap_or(0)); + if let Some(w) = r["counts"].as_object().into_iter().flatten().map(|(o, _)| o.chars().count() as u128).max() { + if w > u128::from(MAX_BITS) { + out.push(format!("rule 29: outcomes of {w} bits, past the {MAX_BITS} a program holds")); + } + if w.saturating_mul(shots) > MAX_RESULT_BITS { + out.push(format!("rule 29: {shots} shots of {w} bits, past the {MAX_RESULT_BITS} bits a run's results hold")); + } + } + // 30: the work a run asks of its host, as much of it as a receipt shows (§4.8). + let n = u128::from(count(&r["qubits"]).unwrap_or(0)); + let instances = if run["state"] == "job" { 1 } else { shots }; + let instance: u128 = mods.iter().map(|m| CALL_WORK.saturating_mul(1 + 2 * priors(m).len() as u128).saturating_add(bytes(&m["bytes"]))).fold(0, u128::saturating_add); + let code: u128 = mods.iter().map(|m| CODE_WORK.saturating_mul(bytes(&m["bytes"]))).fold(0, u128::saturating_add); + let calls = (CALL_WORK + 1).saturating_mul(bytes(&r["callouts"])); + let work = shots.saturating_mul(2 * n * n.div_ceil(32).max(1)).saturating_add(instances.saturating_mul(instance)).saturating_add(code).saturating_add(calls).saturating_add(FUEL_WORK.saturating_mul(u128::from(budget("per_job")))); + if work > MAX_HOST_WORK { + out.push(format!("rule 30: at least {work} operations of a host's work, past the {MAX_HOST_WORK} a run asks")); + } + // 31: the envelope the contract names: its magic and lengths, its manifest, its + // contract and its frames (§2.1, §2.3). + let (envelope, frames) = envelope_bytes(given.then_some(c), r); + if envelope > MAX_ENVELOPE_BYTES as u128 { + out.push(format!("rule 31: an envelope of {envelope} bytes ({frames} of program, modules and priors), past the {MAX_ENVELOPE_BYTES} an envelope holds")); + } + // 32: each call is a step, and a shot runs each step at most once, of 2^21 (§4.8). + let ran_steps = u128::from(count(&r["shots_run"]).unwrap_or(0)) + u128::from(in_call); + if bytes(&r["callouts"]) > MAX_STEPS.saturating_mul(ran_steps) { + out.push(format!("rule 32: {} callouts in {ran_steps} shots of at most {MAX_STEPS} steps", r["callouts"])); + } + // 33: a prior passed whole is one stretch under one lifecycle call's limit (§5.2, §5.3). + let whole: &[Value] = if sets >= 1 { mods } else { stopped_set }; + if let Some(p) = whole.iter().flat_map(priors).find(|p| bytes(&p["bytes"]) + 3 > u128::from(budget("per_instance"))) { + out.push(format!("rule 33: a prior of {} bytes passed whole, past the {} a lifecycle call is given", p["bytes"], budget("per_instance"))); + } + if let Some(f) = fault { + // 34: what a fault's reason shows was charged (§3.2, §4.2, §4.3). + // A fault with no reason shows none (rule 18 holds it). + let least = match f.get("reason").map(Value::as_str) { + None | Some(Some("fuel_exhausted")) => 0, + Some(Some("bad_result")) => 2, + Some(Some("stack_exhausted")) => STACK_FAULT_FUEL, + Some(_) => 1, + }; + if fault_fuel.is_some_and(|n| n < least) { + out.push(format!("rule 34: a fault of {} after {} fuel, where it is charged {least} at least", f["reason"], f["fuel"])); + } + } + // 35: with no call returned, the transcript holds only the run's end (§6.4). + if let Some(want) = bare_transcript(r) + && r["transcript_blake3"].as_str() != Some(want.as_str()) + { + out.push(format!("rule 35: a transcript of no returned call is {want}, not {}", r["transcript_blake3"])); + } +} + +/// The bytes of the envelope a contract names (the request's `wasmq`, when it has one), as a +/// result `r` of it shows them, and of its frames (rule 31, §2.1, §2.2): its magic and +/// lengths, its manifest, the contract in RFC 8785 form, and the program, each module and +/// each prior. +pub fn envelope_bytes(given: Option<&Value>, r: &Value) -> (u128, u128) { + // A request with no contract names an envelope whose contract has no bytes. + let c = given.unwrap_or(&Value::Null); + // 31: its frames, its manifest and its contract. + let bytes = |v: &Value| u128::from(count(v).unwrap_or(0)); + let mut frames = bytes(&c["program"]["bytes"]); + for m in c["modules"].as_array().into_iter().flatten() { + frames = frames.saturating_add(bytes(&m["bytes"])); + for p in m["priors"].as_array().into_iter().flatten() { + frames = frames.saturating_add(bytes(&p["bytes"])); + } + } + let digits = |v: &Value| count(v).unwrap_or(0).to_string().len() as u128; + let manifest = MANIFEST_BYTES + digits(&r["qubits"]) + digits(&c["run"]["shots"]) + digits(&c["run"]["seed"]); + let contract = given.map_or(0, |c| super::receipt::canonical(c).len() as u128); + ((ENVELOPE_FRAMING + manifest + contract).saturating_add(frames), frames) +} + +/// The `transcript_blake3` a result in which no call returned has (rule 35, §6.4): the +/// prefix, and after a fault its shot, reason and fuel. Nothing when `callouts` is no count +/// or a call returned. +pub fn bare_transcript(r: &Value) -> Option { + // 35: the run's end, when no call returned. + let fault = if r["status"] == "fault" { r.get("fault").filter(|f| f.is_object()) } else { None }; + let in_call = fault.is_some_and(|f| f["phase"] == "call"); + let k = count(&r["callouts"])?; + if k > u64::from(in_call) { + return None; + } + let mut t = blake3::Hasher::new(); + t.update(TRANSCRIPT_PREFIX); + if let Some(f) = fault { + t.update(b"fault"); + t.update(&count(&f["shot"]).unwrap_or(0).to_le_bytes()); + t.update(f["reason"].as_str().unwrap_or("").as_bytes()); + t.update(&count(&f["fuel"]).unwrap_or(0).to_le_bytes()); + } + Some(t.finalize().as_bytes().iter().map(|b| format!("{b:02x}")).collect()) +} + +/// What a verdict says of `all`: the first [`PROBLEMS_LISTED`], each short, then how many +/// more. A verdict's length does not follow its job's. +fn listed(mut all: Vec) -> Vec { + let more = all.len().saturating_sub(PROBLEMS_LISTED); + all.truncate(PROBLEMS_LISTED); + for p in &mut all { + if p.len() > PROBLEM_BYTES { + let end = (0..=PROBLEM_BYTES).rev().find(|&i| p.is_char_boundary(i)).unwrap_or(0); + p.truncate(end); + p.push('…'); + } + } + if more > 0 { + all.push(format!("and {more} more")); + } + all +} + +/// What the host's unsealed words in a job's body say against what is sealed (§6.5): its +/// `status` and `method` where they differ from what the signed result gives, and members a +/// body of this profile does not hold. A verdict takes nothing from them; a reader is told. +pub fn notes(job: &Value) -> Vec { + let r = &job["result"]; + let mut out = Vec::new(); + for (name, value) in job.as_object().into_iter().flatten() { + match name.as_str() { + "request" | "result" => {} + "status" if value == body_status(r) => {} + "status" => out.push(format!("the host's unsealed status says {value}; the sealed result says {}", Value::from(body_status(r)))), + "method" if value == &r["method"] => {} + "method" => out.push(format!("the host's unsealed method says {value}; the sealed result says {}", r["method"])), + other => out.push(format!("the host's body holds `{other}`, which nothing seals")), + } + } + for name in ["status", "method"] { + if job.get(name).is_none() { + out.push(format!("the host's body has no `{name}`; the sealed result gives it")); + } + } + listed(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A job of the corpus, as the command line sealed it. + fn corpus(name: &str) -> Value { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../quantum-conformance/receipts/jobs/{name}")); + serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap() + } + + #[test] + fn a_number_is_read_by_its_value() { + // A whole number past 2^53 - 1 is no count however a reader holds it: its double is past it. + let big = [(json!(9007199254740991u64), Some(MAX_EXACT)), (json!(9007199254740992u64), None), (json!(10000000000000001u64), None), (json!(u64::MAX), None)]; + for (n, want) in [(json!(1), Some(1)), (json!(1.0), Some(1)), (json!(-0.0), Some(0)), (json!(0.5), None), (json!(-1), None), (json!(1e30), None), (json!("1"), None), (Value::Null, None)].into_iter().chain(big) { + assert_eq!(count(&n), want, "{n}"); + } + } + + #[test] + fn a_job_the_profile_writes_has_no_problem_and_every_kept_job_is_one() { + for name in ["cli-wasmq-run.json", "cli-wasmq-state-job.json", "cli-wasmq-fault.json", "cli-wasmq-fault-run.json", "cli-wasmq-fault-init.json", "cli-wasmq-fault-prior.json"] { + let job = corpus(name); + assert_eq!(problems(&job["request"], &job["result"]), Vec::::new(), "{name}"); + assert_eq!(notes(&job), Vec::::new(), "{name}"); + } + } + + /// A hostile job's verdict is no longer for being longer, and its rules stay linear in + /// its length: a million callouts of fuel are read once each, not once for each extern. + #[test] + fn a_verdict_is_bounded_and_a_long_job_is_read_in_a_line() { + let mut job = corpus("cli-wasmq-run.json"); + let many = 1usize << 18; + let externs: Vec = (0..many).map(|i| json!({ "extern": format!("e{i}"), "module": 0, "export": "x" })).collect(); + let fuel: serde_json::Map = (0..many).map(|i| (format!("f{i}"), json!({ "calls": 0, "fuel": 1, "max": 2 }))).collect(); + job["request"]["wasmq"]["callouts"] = json!(externs); + job["result"]["fuel"]["callouts"] = Value::Object(fuel); + job["request"]["wasmq"]["profile"] = json!("x".repeat(1 << 16)); + let start = std::time::Instant::now(); + let p = problems(&job["request"], &job["result"]); + assert!(start.elapsed().as_secs() < 30, "{:?}", start.elapsed()); + assert_eq!(p.len(), PROBLEMS_LISTED + 1, "{}", p.len()); + assert!(p.last().unwrap().starts_with("and ") && p.iter().all(|x| x.len() <= PROBLEM_BYTES + 4), "{:?}", p.last()); + } + + /// A member's name as long as half the text, over as many numbers as the other half: + /// rule 1's walk reads each name once, not once for each number under it. + #[test] + fn a_long_name_over_many_numbers_is_read_in_a_line() { + let mut job = corpus("cli-wasmq-run.json"); + // Four MiB of name over a million numbers: a walk that copies the name for each number + // copies four TiB, and one that reads it once reads it in milliseconds. + job["request"]["k".repeat(1 << 22)] = json!(vec![0; 1 << 20]); + let start = std::time::Instant::now(); + let p = problems(&job["request"], &job["result"]); + assert!(start.elapsed().as_secs() < 5, "{:?}", start.elapsed()); + assert_eq!(p.len(), 1, "the one member the request does not define: {p:?}"); + } + + /// A budget past 2^53 - 1 is no count, so it gives nothing: a call's largest is past it. + #[test] + fn a_budget_past_two_to_the_53_is_no_budget() { + let mut job = corpus("cli-wasmq-run.json"); + job["request"]["wasmq"]["run"]["fuel"]["per_call"] = json!(10000000000000001u64); + job["result"]["fuel"]["budget"]["per_call"] = json!(10000000000000001u64); + let p = problems(&job["request"], &job["result"]); + assert!(p.iter().any(|x| x.contains("in one call, past the budget 0")), "rule 14 reads the budget as 0: {p:?}"); + } + + /// A fault outside a call used at most its phase's budget, the instance's: one in an + /// init that used 2 under a budget of 1 is a problem alone, and under 2 none. + #[test] + fn a_fault_outside_a_call_is_within_the_instance_s_budget() { + let job = corpus("cli-wasmq-fault-init.json"); + let with = |per_instance: u64| { + let (mut req, mut r) = (job["request"].clone(), job["result"].clone()); + req["wasmq"]["run"]["fuel"]["per_instance"] = json!(per_instance); + r["fuel"]["budget"]["per_instance"] = json!(per_instance); + r["fuel"]["instances"] = json!(2); + r["fuel"]["used"] = json!(2); + r["fault"]["fuel"] = json!(2); + r["transcript_blake3"] = json!(bare_transcript(&r).unwrap()); + problems(&req, &r) + }; + assert_eq!(with(2), Vec::::new(), "the positive control"); + let p = with(1); + assert!(p.len() == 1 && p[0].contains("past the 1 an instance's phase is given"), "{p:?}"); + } + + /// The call that stopped a run is one of the calls made (§6.4): a call's fault whose + /// callout the fuel counts no call of, or that used more than the most one call used, is + /// a contradiction, and each is a problem alone. + #[test] + fn a_call_s_fault_is_one_of_the_calls_made() { + let job = corpus("cli-wasmq-fault.json"); + assert_eq!(problems(&job["request"], &job["result"]), Vec::::new(), "the positive control"); + let mut none = job["result"].clone(); + none["callouts"] = json!(0); + none["fuel"]["callouts"] = json!({}); + none["fuel"]["instances"] = json!(400); + let p = problems(&job["request"], &none); + assert_eq!(p.len(), 1, "{p:?}"); + assert!(p[0].contains("the fuel counts no call of it"), "{p:?}"); + let mut past = job["result"].clone(); + past["callouts"] = json!(2); + past["fuel"]["callouts"]["f"] = json!({ "calls": 2, "fuel": 401, "max": 399 }); + past["fuel"]["used"] = json!(401); + let p = problems(&job["request"], &past); + assert_eq!(p.len(), 1, "{p:?}"); + assert!(p[0].contains("past the most one of its calls used"), "{p:?}"); + } + + /// Past every bound at once, a checked sum and a product that cannot overflow. + #[test] + fn numbers_at_every_size_never_panic() { + let job = corpus("cli-wasmq-fault.json"); + for at in ["shots_run", "shots", "callouts"] { + for value in [json!(u64::MAX), json!(1e300), json!(-1), json!("x"), Value::Null] { + let mut r = job["result"].clone(); + r[at] = value.clone(); + r["fuel"]["instances"] = json!(u64::MAX); + r["fuel"]["used"] = value; + assert!(!problems(&job["request"], &r).is_empty(), "{at}"); + } + } + } + + /// A change to a kept job, and the problems of what it makes: with no call returned, + /// its transcript made again for what it says, as the writer makes it (rule 35). + fn changed(name: &str, change: impl FnOnce(&mut Value, &mut Value)) -> Vec { + let job = corpus(name); + let (mut req, mut r) = (job["request"].clone(), job["result"].clone()); + change(&mut req["wasmq"], &mut r); + if let Some(t) = bare_transcript(&r) + && r["transcript_blake3"] != "0".repeat(64) + { + r["transcript_blake3"] = json!(t); + } + problems(&req, &r) + } + + /// Sets a budget in the contract and in the result, which repeats it. + fn budget(c: &mut Value, r: &mut Value, name: &str, v: u64) { + c["run"]["fuel"][name] = json!(v); + r["fuel"]["budget"][name] = json!(v); + } + + /// The most bytes a prior of `cli-wasmq-fault-prior.json` may have: its envelope's other + /// bytes, its magic and lengths, its manifest and its contract, taken from 2^27 (rule 31). + fn most_prior(c: &mut Value, r: &Value) -> u64 { + c["modules"][0]["priors"][0]["bytes"] = json!(100_000_000u64); + let digits = |v: &Value| v.as_u64().unwrap().to_string().len() as u64; + let manifest = 192 + digits(&r["qubits"]) + digits(&c["run"]["shots"]) + digits(&c["run"]["seed"]); + let rest = 20 + manifest + super::super::receipt::canonical(c).len() as u64 + c["program"]["bytes"].as_u64().unwrap() + c["modules"][0]["bytes"].as_u64().unwrap(); + (1u64 << 27) - rest + } + + /// `n` calls of `decode64` in `cli-wasmq-run.json`, each of 1. + fn calls(r: &mut Value, n: u64) { + r["callouts"] = json!(n); + r["fuel"]["callouts"]["decode64"] = json!({ "calls": n, "fuel": n, "max": 1 }); + r["fuel"]["used"] = json!(r["fuel"]["instances"].as_u64().unwrap() + n); + } + + /// `cli-wasmq-fault.json`'s call, stopped for `reason` after `fuel`. + fn fault_of(r: &mut Value, reason: &str, fuel: u64) { + r["fault"]["reason"] = json!(reason); + r["fault"].as_object_mut().unwrap().shift_remove("budget"); + r["fault"]["fuel"] = json!(fuel); + r["fuel"]["callouts"]["f"] = json!({ "calls": 1, "fuel": fuel, "max": fuel }); + r["fuel"]["used"] = json!(fuel); + } + + /// Rules 20 to 35 but 27 (rule 32 holds it too): each tie the writer keeps between members, broken alone in a job + /// that keeps every other rule, is that rule's problem and no other; and the same job on + /// the writer's side of the tie has none. Each case is a receipt the reference cannot + /// write and a key can seal. + #[test] + fn each_tie_the_writer_keeps_is_a_rule_alone() { + type Change = Box; + fn ch(f: impl FnOnce(&mut Value, &mut Value) + 'static) -> Change { + Box::new(f) + } + let two_modules = |module: &'static str| -> Change { + ch(move |c, r| { + let m2 = json!({ "name": "m2", "blake3": "0".repeat(64), "bytes": 57, "priors": [] }); + c["modules"].as_array_mut().unwrap().push(m2.clone()); + c["callouts"].as_array_mut().unwrap().push(json!({ "extern": "g", "module": 1, "export": "g" })); + r["modules"].as_array_mut().unwrap().push(json!({ "name": "m2", "blake3": m2["blake3"] })); + r["fault"]["module"] = json!(module); + }) + }; + let cases: Vec<(u32, &str, &str, Change, Change)> = vec![ + // 20: calls that returned used nothing; then each 1. + (20, "cli-wasmq-run.json", "calls that returned used 0", ch(|_, r| { + r["fuel"]["callouts"]["decode64"] = json!({ "calls": 32, "fuel": 0, "max": 0 }); + r["fuel"]["used"] = r["fuel"]["instances"].clone(); + }), ch(|_, r| { + r["fuel"]["callouts"]["decode64"] = json!({ "calls": 32, "fuel": 32, "max": 1 }); + r["fuel"]["used"] = json!(r["fuel"]["instances"].as_u64().unwrap() + 32); + })), + // 20: two calls that returned and one that stopped after 4 in 5 fuel; then in 6. + (20, "cli-wasmq-fault-run.json", "calls that returned used 1", ch(|c, r| { + budget(c, r, "per_job", 5); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 5, "max": 4 }); + r["fuel"]["used"] = json!(5); + }), ch(|c, r| { + budget(c, r, "per_job", 6); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 6, "max": 4 }); + r["fuel"]["used"] = json!(6); + })), + // 20: two calls that returned in 46 fuel, where the most one used is 21; then 23. + (20, "cli-wasmq-fault-run.json", "calls that returned used 46", ch(|_, r| { + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 50, "max": 21 }); + }), ch(|_, r| { + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 50, "max": 23 }); + })), + // 20: 32 calls whose most is 21751, in 21751 fuel: 31 used nothing; then 1 each. + (20, "cli-wasmq-run.json", "less than its max", ch(|_, r| { + r["fuel"]["callouts"]["decode64"] = json!({ "calls": 32, "fuel": 21751, "max": 21751 }); + r["fuel"]["used"] = json!(r["fuel"]["instances"].as_u64().unwrap() + 21751); + }), ch(|_, r| { + r["fuel"]["callouts"]["decode64"] = json!({ "calls": 32, "fuel": 21751 + 31, "max": 21751 }); + r["fuel"]["used"] = json!(r["fuel"]["instances"].as_u64().unwrap() + 21751 + 31); + })), + // 20: two calls that returned in 23, the most 23, beside a stopped call of 4: the + // other used nothing; then 1. + (20, "cli-wasmq-fault-run.json", "less than its max", ch(|c, r| { + budget(c, r, "per_job", 27); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 27, "max": 23 }); + r["fuel"]["used"] = json!(27); + }), ch(|c, r| { + budget(c, r, "per_job", 28); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 28, "max": 23 }); + r["fuel"]["used"] = json!(28); + })), + // 20: a call that stopped at 0, the run's budget spent, after two that returned in + // 10, the most 10: the other used nothing; then 1. + (20, "cli-wasmq-fault-run.json", "2 calls that returned used 10, less than its max", ch(|c, r| { + budget(c, r, "per_job", 10); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 10, "max": 10 }); + r["fuel"]["used"] = json!(10); + r["fault"]["fuel"] = json!(0); + }), ch(|c, r| { + budget(c, r, "per_job", 11); + r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 11, "max": 10 }); + r["fuel"]["used"] = json!(11); + r["fault"]["fuel"] = json!(0); + })), + // 21: 32 shots of instances passed a prior of 87 bytes in less than 32 × 90 fuel. + (21, "cli-wasmq-run.json", "making 32 whole sets", ch(|_, r| { + r["fuel"]["instances"] = json!(32 * 90 - 1); + r["fuel"]["used"] = json!(32 * 90 - 1 + 588348); + }), ch(|_, r| { + r["fuel"]["instances"] = json!(32 * 90); + r["fuel"]["used"] = json!(32 * 90 + 588348); + })), + // 21: the one set of a job-state run passed a prior of 231 bytes in 233. + (21, "cli-wasmq-state-job.json", "making 1 whole sets", ch(|_, r| { + r["fuel"]["instances"] = json!(233); + r["fuel"]["used"] = json!(233 + 741532); + }), ch(|_, r| { + r["fuel"]["instances"] = json!(234); + r["fuel"]["used"] = json!(234 + 741532); + })), + // 22: a call of f, module 0's, that stopped in module 1. + (22, "cli-wasmq-fault.json", "not the module of the callout", two_modules("m2"), two_modules("m")), + // 23: a prior's fault in a module with none. + (23, "cli-wasmq-fault-init.json", "which has none", ch(|_, r| r["fault"]["phase"] = json!("prior")), ch(|_, r| r["fault"]["phase"] = json!("seed"))), + // 24: a result that is not one, from an init. + (24, "cli-wasmq-fault-init.json", "a bad_result in phase", ch(|_, r| { + r["fault"]["reason"] = json!("bad_result"); + r["fault"]["fuel"] = json!(2); + r["fuel"]["instances"] = json!(2); + r["fuel"]["used"] = json!(2); + }), ch(|_, r| r["fault"]["reason"] = json!("integer_divide_by_zero"))), + // 25: an init that used 2 in shot 1, where the instances used 1. + (25, "cli-wasmq-fault-init.json", "past the instances' 1", ch(|_, r| { + r["shots_run"] = json!(1); + r["counts"] = json!({ "0": 1 }); + r["callouts"] = json!(1); + r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 5, "max": 5 } }); + r["fuel"]["used"] = json!(6); + r["fault"]["shot"] = json!(1); + r["fault"]["fuel"] = json!(2); + }), ch(|_, r| { + // Shot 0's init used 1 at least, shown by the fault, and the stopped one 2. + r["shots_run"] = json!(1); + r["counts"] = json!({ "0": 1 }); + r["callouts"] = json!(1); + r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 5, "max": 5 } }); + r["fuel"]["instances"] = json!(3); + r["fuel"]["used"] = json!(8); + r["fault"]["shot"] = json!(1); + r["fault"]["fuel"] = json!(2); + })), + // 25: a module's init that stopped after 1, where the 100-byte prior passed to it + // before took 103 at least, in 103 of instances; then in 104. + (25, "cli-wasmq-fault-init.json", "less the 103 the calls before it took", ch(|c, r| { + c["modules"][0]["priors"] = json!([{ "name": "p", "blake3": "0".repeat(64), "bytes": 100 }]); + r["fuel"]["instances"] = json!(103); + r["fuel"]["used"] = json!(103); + }), ch(|c, r| { + c["modules"][0]["priors"] = json!([{ "name": "p", "blake3": "0".repeat(64), "bytes": 100 }]); + r["fuel"]["instances"] = json!(104); + r["fuel"]["used"] = json!(104); + })), + // 26: the one instance of a job-state run, failing in shot 2. + (26, "cli-wasmq-fault-init.json", "where it is made in shot 0", ch(|c, r| { + c["run"]["state"] = json!("job"); + r["shots_run"] = json!(2); + r["counts"] = json!({ "0": 2 }); + r["callouts"] = json!(2); + r["fuel"]["callouts"] = json!({ "f": { "calls": 2, "fuel": 2, "max": 1 } }); + // The one set's init, which the fault shows, and the stopped one. + r["fuel"]["instances"] = json!(2); + r["fuel"]["used"] = json!(4); + r["fault"]["shot"] = json!(2); + }), ch(|_, r| { + r["shots_run"] = json!(2); + r["counts"] = json!({ "0": 2 }); + r["callouts"] = json!(2); + r["fuel"]["callouts"] = json!({ "f": { "calls": 2, "fuel": 2, "max": 1 } }); + // Two sets' inits and the stopped one. + r["fuel"]["instances"] = json!(3); + r["fuel"]["used"] = json!(5); + r["fault"]["shot"] = json!(2); + })), + // 28: a call's fuel fault after 10 of its 400. + (28, "cli-wasmq-fault.json", "where the call's limit is 400", ch(|_, r| { + r["fuel"]["callouts"]["f"] = json!({ "calls": 1, "fuel": 10, "max": 10 }); + r["fuel"]["used"] = json!(10); + r["fault"]["fuel"] = json!(10); + }), ch(|c, r| { + budget(c, r, "per_call", 10); + r["fuel"]["callouts"]["f"] = json!({ "calls": 1, "fuel": 10, "max": 10 }); + r["fuel"]["used"] = json!(10); + r["fault"]["fuel"] = json!(10); + })), + // 28: an instance's fuel fault after 1 of its 10^8. + (28, "cli-wasmq-fault-init.json", "where the instance's limit is 100000000", ch(|_, r| { + r["fault"]["reason"] = json!("fuel_exhausted"); + r["fault"]["budget"] = json!("instance"); + }), ch(|c, r| { + budget(c, r, "per_instance", 1); + r["fault"]["reason"] = json!("fuel_exhausted"); + r["fault"]["budget"] = json!("instance"); + })), + // 28: the run's budget run out with 50 of 60 used. + (28, "cli-wasmq-fault-run.json", "with 50 of 60 used", ch(|c, r| budget(c, r, "per_job", 60)), ch(|_, _| {})), + // 28: the run's budget the limit of a stretch whose own was no more. + (28, "cli-wasmq-fault-run.json", "whose own limit is 23", ch(|c, r| { + budget(c, r, "per_call", 23); + r["fault"]["fuel"] = json!(23); + }), ch(|c, r| { + budget(c, r, "per_call", 24); + r["fault"]["fuel"] = json!(23); + })), + // 29: an outcome of 2^20 + 1 bits; then of 2^20. + (29, "cli-wasmq-run.json", "past the 1048576 a program holds", ch(|_, r| r["counts"] = json!({ "0".repeat((1 << 20) + 1): 32 })), ch(|_, r| r["counts"] = json!({ "0".repeat(1 << 20): 32 }))), + // 29: 2^20 shots of 257 bits; then of 256. + (29, "cli-wasmq-fault-run.json", "bits a run's results hold", ch(|c, r| { + c["run"]["shots"] = json!(1 << 20); + r["shots"] = json!(1 << 20); + r["counts"] = json!({ "0".repeat(257): 2 }); + }), ch(|c, r| { + c["run"]["shots"] = json!(1 << 20); + r["shots"] = json!(1 << 20); + r["counts"] = json!({ "0".repeat(256): 2 }); + })), + // 30: 8 × 2^39 of fuel beside a module's code; then the most the module leaves. + (30, "cli-wasmq-run.json", "operations of a host's work", ch(|c, r| budget(c, r, "per_job", 1 << 39)), ch(|c, r| { + let rest: u64 = 32 * 2 * 17 + 32 * (4096 * 3 + 39442) + 131072 * 39442 + 4097 * 32; + budget(c, r, "per_job", ((1u64 << 42) - rest) / 8); + })), + // 30: 2^15 qubits over 2^20 shots; then over 2^10. + (30, "cli-wasmq-fault-run.json", "operations of a host's work", ch(|c, r| { + c["run"]["shots"] = json!(1 << 20); + r["shots"] = json!(1 << 20); + r["qubits"] = json!(1 << 15); + }), ch(|c, r| { + c["run"]["shots"] = json!(1 << 10); + r["shots"] = json!(1 << 10); + r["qubits"] = json!(1 << 15); + })), + // 31: an envelope one byte past 2^27, its prior the most that a smaller one holds + // and one more; then the most. + (31, "cli-wasmq-fault-prior.json", "past the 134217728 an envelope holds", ch(|c, r| { + let most = most_prior(c, r); + c["modules"][0]["priors"][0]["bytes"] = json!(most + 1); + }), ch(|c, r| { + let most = most_prior(c, r); + c["modules"][0]["priors"][0]["bytes"] = json!(most); + })), + // 32: 32 shots of 2^21 calls and one more; then 2^21 each. + (32, "cli-wasmq-run.json", "callouts in 32 shots", ch(|_, r| calls(r, 32 << 21 | 1)), ch(|_, r| calls(r, 32 << 21))), + // 33: a prior of 87 bytes passed whole under 89 a lifecycle call; then under 90. + (33, "cli-wasmq-run.json", "a prior of 87 bytes passed whole", ch(|c, r| { + budget(c, r, "per_instance", 89); + r["fuel"]["instances"] = json!(32 * 90); + r["fuel"]["used"] = json!(32 * 90 + 588348); + }), ch(|c, r| { + budget(c, r, "per_instance", 90); + r["fuel"]["instances"] = json!(32 * 90); + r["fuel"]["used"] = json!(32 * 90 + 588348); + })), + // 34: a trap after nothing charged; then after 1. + (34, "cli-wasmq-fault-init.json", "a fault of \"unreachable\" after 0 fuel", ch(|_, r| { + r["fault"]["fuel"] = json!(0); + r["fuel"]["used"] = json!(1); + }), ch(|_, _| {})), + // 34: a result that is not one, after 1; then after 2. + (34, "cli-wasmq-fault.json", "a fault of \"bad_result\" after 1 fuel", ch(|_, r| fault_of(r, "bad_result", 1)), ch(|_, r| fault_of(r, "bad_result", 2))), + // 34: the stack's weight past 2^23 after 787; then after 788. + (34, "cli-wasmq-fault.json", "a fault of \"stack_exhausted\" after 787 fuel", ch(|c, r| { + budget(c, r, "per_call", 1000); + fault_of(r, "stack_exhausted", 787); + }), ch(|c, r| { + budget(c, r, "per_call", 1000); + fault_of(r, "stack_exhausted", 788); + })), + // 35: a transcript of no returned call that is not the run's end; then the one. + (35, "cli-wasmq-fault-init.json", "a transcript of no returned call", ch(|_, r| r["transcript_blake3"] = json!("0".repeat(64))), ch(|_, _| {})), + ]; + for (rule, name, says, broken, kept) in cases { + let p = changed(name, broken); + assert!(p.len() == 1 && p[0].contains(says), "rule {rule} on {name}: {says:?} alone, not {p:?}"); + assert_eq!(changed(name, kept), Vec::::new(), "rule {rule} on {name}: the writer's side"); + } + } + + /// The rules each broken by a change, by their numbers. + fn rules_of(p: &[String]) -> Vec { + let mut r: Vec = p.iter().filter_map(|x| x.strip_prefix("rule ")?.split(':').next()?.parse().ok()).collect(); + r.dedup(); + r + } + + /// A name's length is its characters (rule 29): one of 256 code points in 257 bytes is + /// within 2^28 over 2^20 shots, and breaks only rule 19; one more is past it. And the + /// longest name is the one held, beside a shorter one within the bound. + #[test] + fn a_name_is_as_long_as_its_characters_and_the_longest_is_held() { + let wide = |name: String| { + changed("cli-wasmq-fault-run.json", move |c, r| { + c["run"]["shots"] = json!(1 << 20); + r["shots"] = json!(1 << 20); + r["counts"] = json!({ name: 2 }); + }) + }; + assert_eq!(rules_of(&wide(format!("é{}", "0".repeat(255)))), vec![19]); + assert_eq!(rules_of(&wide(format!("é{}", "0".repeat(256)))), vec![19, 29]); + let two = changed("cli-wasmq-run.json", |_, r| { + let short = r["counts"].as_object().unwrap().keys().next().unwrap().clone(); + r["counts"] = json!({ short: 31, "0".repeat((1 << 20) + 1): 1 }); + }); + assert_eq!(rules_of(&two), vec![19, 29], "{two:?}"); + } + + /// What a lifecycle fault shows of the set it stopped (rules 21 and 25): a module's init + /// that stopped after its 100-byte prior passed took 104 at least, and 102 breaks both + /// rules; a seed that stopped in shot 1 shows shot 0's seed was called. + #[test] + fn a_lifecycle_fault_shows_what_its_set_took() { + let prior = json!([{ "name": "p", "blake3": "0".repeat(64), "bytes": 100 }]); + let p = changed("cli-wasmq-fault-init.json", move |c, r| { + c["modules"][0]["priors"] = prior; + r["fuel"]["instances"] = json!(102); + r["fuel"]["used"] = json!(102); + }); + assert_eq!(rules_of(&p), vec![21, 25], "{p:?}"); + let seed = |instances: u64| { + changed("cli-wasmq-fault-init.json", move |_, r| { + r["shots_run"] = json!(1); + r["counts"] = json!({ "0": 1 }); + r["callouts"] = json!(1); + r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 5, "max": 5 } }); + r["fuel"]["instances"] = json!(instances); + r["fuel"]["used"] = json!(instances + 5); + r["fault"]["phase"] = json!("seed"); + r["fault"]["shot"] = json!(1); + }) + }; + assert_eq!(rules_of(&seed(1)), vec![25]); + assert_eq!(rules_of(&seed(2)), Vec::::new()); + // The first shot's instances failing after a call: rule 27, and rule 32 with it, since + // no shot ran its steps. Rule 27 is never broken alone. + let p = changed("cli-wasmq-fault-init.json", |_, r| { + r["callouts"] = json!(1); + r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 1, "max": 1 } }); + r["fuel"]["used"] = json!(2); + }); + assert_eq!(rules_of(&p), vec![27, 32], "{p:?}"); + } +} diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index a489806f2..49d5e0301 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -288,6 +288,66 @@ pub enum Instr { Jump(usize), } +/// A step of a program that calls outside itself ([`read_callouts`]): a step of +/// [`Instr`], or a call of an `extern`. +#[derive(Clone, Debug, PartialEq)] +pub enum Step { + Instr(Instr), + /// Call the `extern` [`Hybrid::externs`]`[func]`: its arguments' bits, in + /// the order declared and the lowest of each first, go in, and what comes + /// back goes into `output`, the lowest bit first. + Callout { func: u32, input: Vec, output: Vec }, +} + +/// A program read with its calls of `extern`s ([`read_callouts`]). +#[derive(Clone, Debug)] +pub struct Hybrid { + /// The program as [`read_with`] reads it, with each call a step that does + /// nothing (a [`Instr::Jump`] to the next step) in `program.dynamic`: run + /// [`Hybrid::steps`] instead, which holds the calls. + pub reading: Reading, + /// The program's steps, its calls among them. + pub steps: Vec, + /// The `extern`s the calls call, in the order declared. + pub externs: Vec, +} + +/// A bit a callout takes: one the run writes, or one known on reading. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CalloutBit { + Bit(u32), + Const(bool), +} + +/// The type of an `extern`'s argument. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ExternArg { + /// `bit[n]`. + Bit(u32), + /// `uint[n]`, at most 64 bits. + Uint(u32), + /// `int[n]`, at most 64 bits, two's complement. + Int(u32), +} + +impl ExternArg { + /// The bits it takes. + pub fn bits(self) -> u32 { + match self { + ExternArg::Bit(n) | ExternArg::Uint(n) | ExternArg::Int(n) => n, + } + } +} + +/// An `extern` a program declares, as [`read_callouts`] read it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExternDecl { + pub name: String, + pub args: Vec, + /// The width of the `bit[n]` it returns; `None` when it returns nothing. + pub ret: Option, +} + // --------------------------------------------------------------------------- // What a program carries beyond its gates @@ -408,6 +468,54 @@ pub fn read_with(src: &str, inputs: &Inputs) -> Result { Ok(Reading { program, lines, diagnostics }) } +/// [`read_with`], with calls of `extern` functions read as [`Step::Callout`] +/// steps instead of refused: a signature of `bit[n]`, `uint[k]` and `int[k]` +/// (k at most 64) that returns `bit[n]` or nothing, the arguments bits or values +/// known on reading, and what comes back written to bits. The hybrid profile +/// (extensions/quantum-wasmq.md §5) reads programs this way and says what a call does; every other +/// surface reads with [`read_with`], which refuses the call ([`diag::EXTERN_CALL`]). +pub fn read_callouts(src: &str, inputs: &Inputs) -> Result { + read_callouts_within(src, inputs, Bounds::NONE) +} + +/// What a reader that runs programs from others holds a program to as it reads it, so that +/// a short program cannot make it hold or do more than a host of any size has before it is +/// refused ([`diag::BOUND`], or [`diag::TOO_MANY_QUBITS`]). The hybrid profile gives its own +/// (extensions/quantum-wasmq.md §4.8). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bounds { + /// The most qubits. + pub qubits: u32, + /// The most bits: the program's and its blocks' own together, any one value's, and the + /// elements of any one array. + pub bits: u32, + /// The most steps. + pub steps: usize, + /// The most bits the calls of `extern`s take and give, summed over the steps. + pub callout_bits: u64, + /// The most work reading may do: one for each statement and loop iteration, for each + /// value worked out one and one more for each of its bits or elements that it makes, + /// and one for each bit a program declares. + pub reading: u64, +} + +impl Bounds { + /// No bound past the reader's own. + pub const NONE: Bounds = Bounds { qubits: u32::MAX, bits: u32::MAX, steps: usize::MAX, callout_bits: u64::MAX, reading: u64::MAX }; +} + +/// [`read_callouts`], holding the program to `bounds` as it is read. +pub fn read_callouts_within(src: &str, inputs: &Inputs, bounds: Bounds) -> Result { + let toks = lex::lex(src)?; + let (stmts, version, depths) = parse::parse_depths(src, &toks)?; + let (program, lines, diagnostics, externs, calls) = elab::elaborate_callouts(src, &toks, &stmts, &depths, version, inputs, bounds)?; + let mut steps: Vec = program.dynamic.iter().flatten().cloned().map(Step::Instr).collect(); + for (at, func, input, output) in calls { + steps[at] = Step::Callout { func, input, output }; + } + Ok(Hybrid { reading: Reading { program, lines, diagnostics }, steps, externs }) +} + /// [`read`], each gate whose standard form would double with its controls /// kept as one op ([`Op::is_whole`]), under up to 255 controls: for a method /// that runs such a gate as one. Everything else reads as [`read`] reads it, diff --git a/wai/wai-quantum/src/qasm/diag.rs b/wai/wai-quantum/src/qasm/diag.rs index 79322e301..04d260f79 100644 --- a/wai/wai-quantum/src/qasm/diag.rs +++ b/wai/wai-quantum/src/qasm/diag.rs @@ -242,6 +242,8 @@ table! { NOWHERE = "Q320", Refused, "A measurement whose result goes nowhere: `measure q;` while bit registers are declared (write `c = measure q;`)."; MEASURE_LENGTHS = "Q321", Refused, "Qubits measured into a different number of bits."; LOCAL_BIT = "Q322", Lowered, "A bit declared inside a block or a subroutine: a bit of its own, which the steps read and the results do not show."; + BOUND = "Q323", Refused, "A program past a bound its reader holds it to as it reads it: qubits, bits, steps, the bits its calls of externs take and give, or the work of reading it (a reader of programs from others, as the hybrid profile reads them)."; + BARE_MEASURE = "Q324", Refused, "A measurement with no target bit (`measure q;`) in a program that calls externs: its outcome is the bits it declares, so each measurement names the bit it writes (write `c = measure q;`)."; // Timing. TIMING_IGNORED = "Q400", Warned, "The method applies no durations: the program's delays, boxes and stretches change nothing it computes, and the result says so."; diff --git a/wai/wai-quantum/src/qasm/elab.rs b/wai/wai-quantum/src/qasm/elab.rs index 07e3dbce2..52866e05f 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -22,7 +22,7 @@ use super::parse::{Depths, Version}; use super::value::{self, Angle, Dur, Ty, Val}; use super::controls::{self, Sink}; use super::{ - Cond, Extra, Inputs, Instr, Kind, MAX_OPS, MAX_QUBITS, Native, NativeBase, Op, Output, OutputValue, Program, Timing, TimingKind, builtin, normalise, + CalloutBit, Cond, ExternArg, ExternDecl, Extra, Inputs, Instr, Kind, MAX_OPS, MAX_QUBITS, Native, NativeBase, Op, Output, OutputValue, Program, Timing, TimingKind, builtin, normalise, }; /// How deep gate definitions and subroutine calls may nest. @@ -278,6 +278,20 @@ pub(crate) struct Elab<'a> { gates: HashMap>, defs: HashMap>, externs: HashSet, + /// Calls of `extern`s are read as steps (`read_callouts`), not refused. + callouts: bool, + /// The program's tokens, from which an `extern`'s signature is read. + toks: &'a [Token], + /// The `extern`s calls are read against, in the order declared. + extern_decls: Vec, + /// Each call: the step that holds its place, the `extern`, its bits in and out. + calls: Vec, + /// What the program is held to as it is read (`qasm::read_callouts_within`). + bounds: super::Bounds, + /// The work reading has done, as [`super::Bounds::reading`] counts it. + reading: u64, + /// The bits the calls read so far take and give. + callout_bits: u64, opaque: HashSet, calibrated: HashSet, taken: HashMap, @@ -313,7 +327,7 @@ pub(crate) fn elaborate(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Dept /// [`elaborate`], a gate whose standard form would double staying one op /// ([`super::controls`]). pub(crate) fn elaborate_whole(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, version: Version, inputs: &Inputs) -> R<(Program, Vec, Vec)> { - elaborate_form(src, toks, stmts, depths, version, inputs, None, true).map(|(p, l, d, _)| (p, l, d)) + elaborate_full(src, toks, stmts, depths, version, inputs, None, false, true).map(|(p, l, d, _)| (p, l, d)) } /// One op's parameters' derivatives by each of the program's parameters. @@ -323,12 +337,52 @@ pub(crate) type OpTangents = Vec>; /// `tan` names ([`Tan`]): for each op, for each of its parameters, its /// derivative by each name. pub(crate) fn elaborate_with(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, version: Version, inputs: &Inputs, tan: Option) -> R<(Program, Vec, Vec, Vec)> { - elaborate_form(src, toks, stmts, depths, version, inputs, tan, false) + elaborate_full(src, toks, stmts, depths, version, inputs, tan, false, false) } -#[allow(clippy::too_many_arguments, clippy::type_complexity)] -fn elaborate_form(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, version: Version, inputs: &Inputs, tan: Option, whole: bool) -> R<(Program, Vec, Vec, Vec)> { - let mut e = Elab { +/// [`elaborate`] with each call of an `extern` read as a step (`qasm::read_callouts`). +/// A reading of a program with its calls: the program, its lines, its diagnostics, its +/// `extern`s and its calls. +pub(crate) type CalloutReading = (Program, Vec, Vec, Vec, Vec); + +pub(crate) fn elaborate_callouts(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, version: Version, inputs: &Inputs, bounds: super::Bounds) -> R { + let mut e = elab(src, toks, depths, version, inputs, None, true, false); + e.bounds = bounds; + e.prescan(toks, stmts)?; + e.block(stmts)?; + let externs = std::mem::take(&mut e.extern_decls); + let (p, l, d, calls) = e.finish_with_calls()?; + // The steps a statement's jumps add are counted here, all at once. + let steps = p.dynamic.as_ref().map_or(0, Vec::len); + if steps > bounds.steps { + return Err(Diagnostic::new(diag::BOUND, None, format!("the program takes {steps} steps; it is held to {}", bounds.steps))); + } + Ok((p, l, d, externs, calls)) +} + +/// A call of an `extern`: the step that holds its place, the `extern`, its bits in and out. +pub(crate) type Call = (usize, u32, Vec, Vec); + +/// The calls of a reading, in the order they were read. +type CallList = Vec; + +/// A reading: the program, its lines, its diagnostics, and its ops' tangents. +type Elaborated = (Program, Vec, Vec, Vec); + +#[allow(clippy::too_many_arguments)] +fn elaborate_full(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, version: Version, inputs: &Inputs, tan: Option, callouts: bool, whole: bool) -> R { + let mut e = elab(src, toks, depths, version, inputs, tan, callouts, whole); + e.prescan(toks, stmts)?; + e.block(stmts)?; + let tangents = e.tan.take().map(|t| t.ops).unwrap_or_default(); + let (p, l, d) = e.finish()?; + debug_assert!(tangents.is_empty() || tangents.len() == p.ops.len()); + Ok((p, l, d, tangents)) +} + +#[allow(clippy::too_many_arguments)] +fn elab<'a>(src: &'a str, toks: &'a [Token], depths: &'a Depths, version: Version, inputs: &'a Inputs, tan: Option, callouts: bool, whole: bool) -> Elab<'a> { + Elab { src, depths, nest: 0, @@ -352,6 +406,13 @@ fn elaborate_form(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, ve gates: HashMap::new(), defs: HashMap::new(), externs: HashSet::new(), + callouts, + toks, + extern_decls: Vec::new(), + calls: Vec::new(), + bounds: super::Bounds::NONE, + reading: 0, + callout_bits: 0, opaque: HashSet::new(), calibrated: HashSet::new(), taken: HashMap::new(), @@ -370,13 +431,7 @@ fn elaborate_form(src: &str, toks: &[Token], stmts: &[Stmt], depths: &Depths, ve tan, whole, held: 0, - }; - e.prescan(toks, stmts)?; - e.block(stmts)?; - let tangents = e.tan.take().map(|t| t.ops).unwrap_or_default(); - let (p, l, d) = e.finish()?; - debug_assert!(tangents.is_empty() || tangents.len() == p.ops.len()); - Ok((p, l, d, tangents)) + } } /// The reader as what a controlled gate's lowering writes to: each gate an op @@ -425,9 +480,36 @@ impl<'a> Elab<'a> { if self.count > MAX_STEPS { return self.fail(diag::UNROLL_LIMIT, at, format!("the program unrolls past {MAX_STEPS} statements")); } + self.charge(1, at)?; + self.room(0, at) + } + + /// `units` more of the work of reading ([`super::Bounds::reading`]). + fn charge(&mut self, units: u64, at: Loc) -> R<()> { + self.reading = self.reading.saturating_add(units); + if self.reading > self.bounds.reading { + return self.fail(diag::BOUND, at, format!("reading the program takes more than the {} units of work it is held to", self.bounds.reading)); + } Ok(()) } + /// Whether `more` steps fit the bound on steps. + fn room(&mut self, more: usize, at: Loc) -> R<()> { + if self.steps.len().saturating_add(more) > self.bounds.steps { + return self.fail(diag::BOUND, at, format!("the program takes more than the {} steps it is held to", self.bounds.steps)); + } + Ok(()) + } + + /// Whether `n` more bits of the program's or a block's own fit the bound on bits. + fn bit_room(&mut self, n: u32, at: Loc) -> R<()> { + let held = u64::from(self.n_bits) + u64::from(self.hidden) + u64::from(n); + if held > u64::from(self.bounds.bits) { + return self.fail(diag::BOUND, at, format!("{held} bits; the program is held to {}", self.bounds.bits)); + } + self.charge(u64::from(n), at) + } + // ----------------------------------------------------------------------- // Before running: physical qubits, and the names registers and gates take // ----------------------------------------------------------------------- @@ -438,9 +520,10 @@ impl<'a> Elab<'a> { _ => None, }); if let Some(m) = max.max() { - if m >= MAX_QUBITS { + if m >= MAX_QUBITS || m >= self.bounds.qubits { let t = toks.iter().find(|t| matches!(&t.tok, Tok::Ident(w) if is_physical(w) == Some(m))).expect("found above"); - return Err(Diagnostic::new(diag::TOO_MANY_QUBITS, Some(t.span(self.src)), format!("${m}: this service reads programs of at most {MAX_QUBITS} qubits"))); + let most = MAX_QUBITS.min(self.bounds.qubits); + return Err(Diagnostic::new(diag::TOO_MANY_QUBITS, Some(t.span(self.src)), format!("${m}: this service reads programs of at most {most} qubits"))); } self.physical = true; self.grow(m + 1); @@ -538,6 +621,7 @@ impl<'a> Elab<'a> { self.dynamic = true; } } + self.room(1, at)?; if self.ops.len() >= MAX_OPS { return self.fail(diag::TOO_MANY_GATES, at, format!("more than {MAX_OPS} gates")); } @@ -607,6 +691,7 @@ impl<'a> Elab<'a> { if qs.len() != bs.len() { return self.fail(diag::MEASURE_LENGTHS, at, format!("{} qubits measured into {} bits", qs.len(), bs.len())); } + self.room(qs.len(), at)?; for (&q, &b) in qs.iter().zip(bs) { if self.ones.contains(&b) && self.runtime > 0 { return self.fail( @@ -633,6 +718,109 @@ impl<'a> Elab<'a> { Ok(()) } + /// Records an `extern` a program calls as a step: what it takes and returns + /// as the callout's types. + fn declare_extern(&mut self, name: &Name, sig: Option<&(Vec, Option)>, at: Loc) -> R<()> { + let Some((args, ret)) = sig else { + return self.fail(diag::EXTERN_CALL, at, format!("{}: a callout's arguments are written as types, `extern f(bit[8], uint[32]) -> bit[4];`", name.text)); + }; + if self.extern_decls.iter().any(|d| d.name == name.text) { + return self.fail(diag::DECLARED_TWICE, name.at, format!("{} is declared twice", name.text)); + } + let mut decl = ExternDecl { name: name.text.clone(), args: Vec::with_capacity(args.len()), ret: None }; + for a in args { + let t = self.ty(a)?; + decl.args.push(match t { + Ty::Bit(n) => ExternArg::Bit(n.unwrap_or(1)), + Ty::Uint(Some(n)) if (1..=64).contains(&n) => ExternArg::Uint(n), + Ty::Int(Some(n)) if (1..=64).contains(&n) => ExternArg::Int(n), + other => return self.fail(diag::EXTERN_CALL, at, format!("{}: a callout takes `bit[n]`, `uint[k]` and `int[k]`, k from 1 to 64, not {}", name.text, other.name())), + }); + } + decl.ret = match ret { + None => None, + Some(r) => match self.ty(r)? { + Ty::Bit(n) => Some(n.unwrap_or(1)), + other => return self.fail(diag::EXTERN_CALL, at, format!("{}: a callout returns `bit[n]` or nothing, not {}: the reader holds no other value set at run time", name.text, other.name())), + }, + }; + self.extern_decls.push(decl); + Ok(()) + } + + /// A call of an `extern`, as a step that reads bits and writes `target`. + fn callout(&mut self, f: &Name, args: &[Expr], target: Option<&[u32]>, at: Loc) -> R<()> { + let Some(func) = self.extern_decls.iter().position(|d| d.name == f.text) else { + return self.fail(diag::EXTERN_CALL, at, format!("{} has no signature the reader can call", f.text)); + }; + let decl = self.extern_decls[func].clone(); + if args.len() != decl.args.len() { + return self.fail(diag::ARITY, at, format!("{} takes {} arguments, not {}", f.text, decl.args.len(), args.len())); + } + let mut input = Vec::new(); + for (a, ty) in args.iter().zip(&decl.args) { + let want = ty.bits() as usize; + match self.ev(a)? { + Ev::Bits(bits, _) => { + if bits.len() != want { + return self.fail(diag::WRONG_TYPE, a.at, format!("{} takes {want} bits here, not {}", f.text, bits.len())); + } + input.extend(bits.iter().map(|&b| match self.bit_known(b) { + Some(v) => CalloutBit::Const(v), + None => CalloutBit::Bit(b), + })); + } + Ev::Val(Val::Bits(vals)) => { + if vals.len() != want { + return self.fail(diag::WRONG_TYPE, a.at, format!("{} takes {want} bits here, not {}", f.text, vals.len())); + } + input.extend(vals.iter().map(|&v| CalloutBit::Const(v))); + } + Ev::Val(Val::Int(v)) => { + let (lo, hi) = match ty { + ExternArg::Int(n) => (-(1i128 << (n - 1)), (1i128 << (n - 1)) - 1), + _ => (0, (1i128 << want.min(64)) - 1), + }; + if want > 64 || !(lo..=hi).contains(&v) { + return self.fail(diag::WRONG_TYPE, a.at, format!("{v} does not fit the {want} bits {} takes here", f.text)); + } + input.extend((0..want).map(|i| CalloutBit::Const((v >> i) & 1 == 1))); + } + _ => return self.fail(diag::EXTERN_CALL, a.at, format!("{} takes bits, or an integer known on reading", f.text)), + } + } + let output = match (target, decl.ret) { + (None, None) => Vec::new(), + (None, Some(_)) => return self.fail(diag::EXTERN_CALL, at, format!("what {} returns goes into bits: `bit[n] c = {}(...);`", f.text, f.text)), + (Some(_), None) => return self.fail(diag::WRONG_TYPE, at, format!("{} returns nothing", f.text)), + (Some(t), Some(n)) => { + if t.len() != n as usize { + return self.fail(diag::WRONG_TYPE, at, format!("{} returns {n} bits, into {}", f.text, t.len())); + } + t.to_vec() + } + }; + for &b in &output { + if self.ones.contains(&b) && self.runtime > 0 { + return self.fail(diag::BIT_WRITE, at, "a bit set to 1 on reading is written here only under a condition on measured bits: the runs that skip the call would read it as 0"); + } + self.ones.remove(&b); + self.written.insert(b); + } + self.dynamic = true; + self.callout_bits = self.callout_bits.saturating_add((input.len() + output.len()) as u64); + if self.callout_bits > self.bounds.callout_bits { + return self.fail(diag::BOUND, at, format!("the calls of externs take and give more than the {} bits the program is held to", self.bounds.callout_bits)); + } + self.room(1, at)?; + // The call holds its place among the steps as a step that does nothing; the + // reader's caller puts the call there (`qasm::read_callouts`). + let at_step = self.steps.len(); + self.steps.push(Instr::Jump(at_step + 1)); + self.calls.push((at_step, func as u32, input, output)); + Ok(()) + } + /// Bits set to a value known on reading. fn set_bits(&mut self, bits: &[u32], v: &Val) { if let Val::Bits(vals) = v { @@ -899,6 +1087,9 @@ impl<'a> Elab<'a> { if total > MAX_QUBITS as u64 { return self.fail(diag::TOO_MANY_QUBITS, at, format!("{total} qubits; this service reads programs of at most {MAX_QUBITS}")); } + if total > u64::from(self.bounds.qubits) { + return self.fail(diag::TOO_MANY_QUBITS, at, format!("{total} qubits; the program is held to {}", self.bounds.qubits)); + } let start = self.n_qubits; self.grow(total as u32); self.bind(&name.text, Binding::Qubits(Rc::new((start..start + n).collect()))); @@ -951,6 +1142,12 @@ impl<'a> Elab<'a> { } S::Extern { name } => { self.externs.insert(name.text.clone()); + if self.callouts { + self.top_level("extern", at)?; + let k = self.toks.iter().position(|t| t.start == at.start).unwrap_or(self.toks.len()); + let sig = super::parse::Parser::at(self.src, self.toks, k).extern_signature()?; + self.declare_extern(name, sig.as_ref(), at)?; + } } S::Call { mods, name, params, duration, operands } => self.call(mods, name, params, duration.as_ref(), operands, at)?, S::Measure { src, target } => { @@ -961,6 +1158,11 @@ impl<'a> Elab<'a> { if self.has_cregs { return self.fail(diag::NOWHERE, at, "`measure` without a target bit while bit registers are declared"); } + // In a program that calls externs the outcome is the bits it declares, + // so a measurement names the bit it writes (wasmQ §6.1). + if self.callouts { + return self.fail(diag::BARE_MEASURE, at, "`measure` without a target bit in a program that calls externs: name the bit it writes"); + } qs.clone() } }; @@ -973,6 +1175,7 @@ impl<'a> Elab<'a> { if self.runtime > 0 || qs.iter().any(|&q| self.touched[q as usize] || self.measured_qubits[q as usize]) { self.dynamic = true; } + self.room(qs.len(), at)?; self.steps.extend(qs.iter().map(|&q| Instr::Reset(q))); } S::Barrier(ops) => { @@ -994,6 +1197,11 @@ impl<'a> Elab<'a> { S::Assign { target, op, value } => self.assign(target, op, value, at)?, S::Expr(e) => { if let E::Call(name, args) = &e.kind + && self.callouts + && self.externs.contains(&name.text) + { + self.callout(name, args, None, e.at)?; + } else if let E::Call(name, args) = &e.kind && self.defs.contains_key(&name.text) { self.call_def(name, args, None, e.at)?; @@ -1125,8 +1333,22 @@ impl<'a> Elab<'a> { Type::Array(inner, dims) => { let t = self.ty(inner)?; let mut ds = Vec::new(); + // What the array holds: its elements, each of its element's bits. + let mut elements = match &t { + Ty::Bit(n) => u64::from(n.unwrap_or(1)), + _ => 1, + }; for d in dims { - ds.push(self.size(d)? as usize); + let n = self.size(d)?; + elements = elements.saturating_mul(u64::from(n)); + ds.push(n as usize); + } + if elements > u64::from(self.bounds.bits) { + return Err(Diagnostic::new(diag::BOUND, None, format!("an array of {elements} elements and bits; the program is held to {}", self.bounds.bits))); + } + self.reading = self.reading.saturating_add(elements); + if self.reading > self.bounds.reading { + return Err(Diagnostic::new(diag::BOUND, None, format!("reading the program takes more than the {} units of work it is held to", self.bounds.reading))); } Ty::Array(Box::new(t), ds) } @@ -1292,12 +1514,14 @@ impl<'a> Elab<'a> { if self.lookup(&name.text).is_some() { return self.fail(diag::DECLARED_TWICE, name.at, format!("{} is declared twice", name.text)); } + self.bit_room(n, at)?; let start = self.n_bits; self.n_bits = self.n_bits.checked_add(n).ok_or_else(|| self.d(diag::INTEGER_RANGE, at, "too many bits"))?; self.has_cregs = true; Rc::new((start..start + n).collect::>()) } else { self.check_name(name, "bit")?; + self.bit_room(n, at)?; self.alloc_hidden(n) }; self.bind(&name.text, Binding::Bits { bits: bits.clone(), readonly: false }); @@ -1313,6 +1537,9 @@ impl<'a> Elab<'a> { E::Call(f, args) if self.defs.contains_key(&f.text) => { self.call_def(f, args, Some(bits), e.at)?; } + E::Call(f, args) if self.callouts && self.externs.contains(&f.text) => { + self.callout(f, args, Some(&bits), e.at)?; + } E::Call(f, _) if self.externs.contains(&f.text) => { return self.fail(diag::EXTERN_CALL, e.at, format!("{} is an `extern`: its body is outside the program, so the reader cannot run it", f.text)); } @@ -1404,6 +1631,9 @@ impl<'a> Elab<'a> { self.call_def(f, args, Some(bs), value.at)?; return Ok(()); } + E::Call(f, args) if op == "=" && self.callouts && self.externs.contains(&f.text) => { + return self.callout(f, args, Some(&bs), value.at); + } E::Call(f, _) if self.externs.contains(&f.text) => { return self.fail(diag::EXTERN_CALL, value.at, format!("{} is an `extern`: its body is outside the program, so the reader cannot run it", f.text)); } @@ -1753,7 +1983,7 @@ impl<'a> Elab<'a> { }; let most = if self.whole { MAX_WHOLE_CONTROLS } else { MAX_CONTROLS }; if polarity.len() as i128 + n > most as i128 { - return self.fail(diag::TOO_MANY_CONTROLS, *mat, format!("modifiers add at most {most} controls here{}", if self.whole { String::new() } else { format!(", written in standard gates; {HELD_WHOLE}") })); + return self.fail(diag::TOO_MANY_CONTROLS, *mat, format!("modifiers add at most {most} controls here{}", if self.whole || self.callouts { String::new() } else { format!(", written in standard gates; {HELD_WHOLE}") })); } polarity.extend(std::iter::repeat_n(*on, n as usize)); } @@ -2155,6 +2385,13 @@ impl<'a> Elab<'a> { _ => {} } } + for (_, _, input, output) in self.calls.iter_mut().filter(|c| c.0 >= frame.step_start) { + for b in input.iter_mut().filter_map(|b| if let CalloutBit::Bit(b) = b { Some(b) } else { None }).chain(output.iter_mut()) { + if let Some(&x) = map.get(b) { + *b = x; + } + } + } return Ok(Some(Ev::Bits(d.clone(), name.text.clone()))); } return Ok(Some(Ev::Bits(bits, name.text.clone()))); @@ -2175,6 +2412,7 @@ impl<'a> Elab<'a> { let dest = match self.frames.last().and_then(|f| f.dest.clone()) { Some(d) => d, None => { + self.bit_room(qs.len() as u32, at)?; let d = self.alloc_hidden(qs.len() as u32); self.frames.last_mut().expect("a frame").dest = Some(d.clone()); d @@ -2733,7 +2971,26 @@ impl<'a> Elab<'a> { // Expressions // ----------------------------------------------------------------------- + /// An expression's value, as [`Elab::ev_unbounded`] works it out, charged to the work of + /// reading and held to the bound on bits ([`super::Bounds`]). fn ev(&mut self, e: &Expr) -> R { + let v = self.ev_unbounded(e)?; + // A value is as large as it is; its work, what was made for it. A register named + // is shared with its binding, and costs nothing for its size. + let (size, made) = match &v { + Ev::Val(Val::Bits(b)) => (b.len(), b.len()), + Ev::Val(Val::Array(a)) => (a.len(), a.len()), + Ev::Bits(b, _) | Ev::Qubits(b, _) => (b.len(), if Rc::strong_count(b) == 1 { b.len() } else { 0 }), + _ => (0, 0), + }; + if size as u64 > u64::from(self.bounds.bits) { + return self.fail(diag::BOUND, e.at, format!("a value of {size} bits or elements; the program is held to {}", self.bounds.bits)); + } + self.charge(1 + made as u64, e.at)?; + Ok(v) + } + + fn ev_unbounded(&mut self, e: &Expr) -> R { let at = e.at; Ok(match &e.kind { E::Int(v) => { @@ -3475,6 +3732,9 @@ impl<'a> Elab<'a> { None => self.fail(diag::WRONG_TYPE, at, format!("{f} returns no value")), }; } + if self.callouts && self.externs.contains(f) { + return self.fail(diag::EXTERN_CALL, at, format!("what {f} returns goes into bits, in a declaration or an assignment: the reader holds no other value set at run time")); + } if self.externs.contains(f) { return self.fail(diag::EXTERN_CALL, at, format!("{f} is an `extern`: its body is outside the program, so the reader cannot run it")); } @@ -3598,7 +3858,11 @@ impl<'a> Elab<'a> { // The end // ----------------------------------------------------------------------- - fn finish(mut self) -> R<(Program, Vec, Vec)> { + fn finish(self) -> R<(Program, Vec, Vec)> { + self.finish_with_calls().map(|(p, l, d, _)| (p, l, d)) + } + + fn finish_with_calls(mut self) -> R<(Program, Vec, Vec, CallList)> { if self.n_qubits == 0 { return Err(Diagnostic::new(diag::NO_QUBITS, None, "the program declares no qubits")); } @@ -3617,7 +3881,9 @@ impl<'a> Elab<'a> { self.patch(&ends, end); let (mut measured, mut n_bits) = (std::mem::take(&mut self.measured), self.n_bits); let mut steps = std::mem::take(&mut self.steps); - if measured.is_empty() { + // A program that measures nothing reads every qubit at its end; one that calls an + // `extern` does not: its bits, which its calls write, are its outcome. + if measured.is_empty() && !self.callouts { measured = (0..self.n_qubits).map(|q| (q, q)).collect(); n_bits = self.n_qubits; steps.extend((0..self.n_qubits).map(|q| Instr::Measure { qubit: q, bit: q })); @@ -3636,6 +3902,14 @@ impl<'a> Elab<'a> { _ => {} } } + for (_, _, input, output) in &mut self.calls { + input.iter_mut().for_each(|b| { + if let CalloutBit::Bit(b) = b { + remap(b); + } + }); + output.iter_mut().for_each(remap); + } } for (name, at) in std::mem::take(&mut self.outputs) { let value = match self.scopes[0].get(&name) { @@ -3654,7 +3928,7 @@ impl<'a> Elab<'a> { let dynamic = self.dynamic.then_some(steps); debug_assert_eq!(self.lines.len(), self.ops.len()); let program = Program { n_qubits: self.n_qubits, ops: self.ops, measured, n_bits, dynamic, extra: self.extra }; - Ok((program, self.lines, self.diags)) + Ok((program, self.lines, self.diags, self.calls)) } } diff --git a/wai/wai-quantum/src/qasm/lang_tests.rs b/wai/wai-quantum/src/qasm/lang_tests.rs index 086eb153d..2cacf15cd 100644 --- a/wai/wai-quantum/src/qasm/lang_tests.rs +++ b/wai/wai-quantum/src/qasm/lang_tests.rs @@ -608,6 +608,80 @@ fn every_refusal_and_warning_carries_a_code_in_the_table() { let _ = FRAC_PI_4; } +/// Calls of `extern`s read as callout steps only through `read_callouts`; every other +/// reading still refuses them (extensions/quantum-wasmq.md §5.1). +#[test] +fn an_extern_call_is_a_callout_step_only_when_asked_for() { + let src = "qubit[3] q;\nbit[3] s;\nextern dec(bit[3], uint[4]) -> bit[2];\nbit[2] c;\nbit one = 1;\nh q;\ns = measure q;\nfor uint r in [0:1] {\n c = dec(s, r + 5);\n if (c[1]) x q[0];\n}\nmeasure q[0] -> one;\n"; + assert_eq!(read(src).unwrap_err().code, diag::EXTERN_CALL); + let r = super::read_callouts(src, &Inputs::new()).unwrap(); + assert_eq!(r.externs, vec![ExternDecl { name: "dec".into(), args: vec![ExternArg::Bit(3), ExternArg::Uint(4)], ret: Some(2) }]); + let steps = &r.steps; + let calls: Vec<(usize, &Step)> = steps.iter().enumerate().filter(|(_, s)| matches!(s, Step::Callout { .. })).collect(); + assert_eq!(calls.len(), 2, "the loop is unrolled into two calls"); + // In the program's own steps each call holds its place as a step that does nothing. + let plain = r.reading.program.dynamic.as_ref().unwrap(); + assert_eq!(plain.len(), steps.len()); + for (i, _) in &calls { + assert_eq!(plain[*i], Instr::Jump(i + 1)); + } + // The register's bits, then the loop's value known on reading, the lowest bit first. + let Step::Callout { func, input, output } = calls[1].1 else { unreachable!() }; + assert_eq!(*func, 0); + assert_eq!(input[..3], [CalloutBit::Bit(0), CalloutBit::Bit(1), CalloutBit::Bit(2)]); + assert_eq!(input[3..], [CalloutBit::Const(false), CalloutBit::Const(true), CalloutBit::Const(true), CalloutBit::Const(false)]); + assert_eq!(output.len(), 2); + // A condition reads what the call wrote. + assert!(steps.iter().any(|s| matches!(s, Step::Instr(Instr::JumpUnless(c, _)) if c.bits == vec![output[1]]))); + // A bit known on reading goes in as its value (a program's bits end as measurement + // leaves them, so a constant one would be refused at the end). + let k = super::read_callouts("qubit q;\nextern f(bit[2]) -> bit[1];\nbit[2] b = \"00\";\nbit[1] r;\nr = f(b);\nbit fin;\nfin = measure q;", &Inputs::new()).unwrap(); + assert!(k.steps.iter().any(|s| matches!(s, Step::Callout { input, .. } if input == &vec![CalloutBit::Const(false), CalloutBit::Const(false)]))); +} + +#[test] +fn a_callout_s_signature_and_use_are_held_to_what_the_reader_can_run() { + let refused = |src: &str| super::read_callouts(src, &Inputs::new()).map(|_| ()).unwrap_err().code; + for (src, code) in [ + ("qubit q; extern f(float[64]) -> bit; bit b; b = f(1.0);", diag::EXTERN_CALL), + ("qubit q; extern f(bit) -> int[8]; bit b; measure q -> b; int[8] x = f(b);", diag::EXTERN_CALL), + ("qubit q; extern f(int[65]) -> bit; bit b = f(1);", diag::WIDTH), + ("qubit q; extern f(bit[2]) -> bit[2]; bit[3] b; bit[2] c = f(b);", diag::WRONG_TYPE), + ("qubit q; extern f(bit) -> bit[2]; bit b; bit[3] c = f(b);", diag::WRONG_TYPE), + ("qubit q; extern f(bit) -> bit; bit b; f(b, b);", diag::ARITY), + ("qubit q; extern f(bit) -> bit; bit b; f(b);", diag::EXTERN_CALL), + ("qubit q; extern f(uint[2]) -> bit; bit b = f(4);", diag::WRONG_TYPE), + ("qubit q; extern f(bit) -> bit; bit b; if (f(b)) x q;", diag::EXTERN_CALL), + ] { + assert_eq!(refused(src), code, "{src}"); + } + // The positive control: the same shapes, held to their signatures, read. + assert!(super::read_callouts("qubit q; extern f(bit[2], int[8]) -> bit[2]; bit[2] b; measure q -> b[0]; bit[2] c = f(b, -3); f2(c);".replace("f2(c);", "").as_str(), &Inputs::new()).is_ok()); +} + +/// In a program that calls externs the outcome is the bits it declares (wasmQ §6.1), so a +/// measurement names the bit it writes: one that names none is refused there, where a +/// reading without calls takes it. +#[test] +fn a_measure_with_no_bit_is_refused_where_a_program_calls_externs() { + let refused = super::read_callouts("qubit[4] q; extern f(uint[8]); f(1); x q[3]; measure q[3];", &Inputs::new()).map(|_| ()).unwrap_err(); + assert_eq!(refused.code, "Q324", "{refused:?}"); + assert!(read("qubit[4] q; x q[3]; measure q[3];").is_ok(), "a reading without calls takes it"); + assert!(super::read_callouts("qubit[4] q; bit[4] c; extern f(uint[8]); f(1); x q[3]; c[3] = measure q[3];", &Inputs::new()).is_ok(), "the bit named"); +} + +/// Nesting past what a reader's stack holds has a code of its own, so that `Q209` keeps the +/// meaning it was released with: definitions and calls nested past 32. +#[test] +fn nesting_past_the_bound_has_its_own_code() { + let deep = format!("qubit q; int x = {}1{};", "(".repeat(200), ")".repeat(200)); + assert_eq!(read(&deep).unwrap_err().code, "Q130"); + let blocks = format!("qubit q; {}x q;{}", "{ ".repeat(200), " }".repeat(200)); + assert_eq!(read(&blocks).unwrap_err().code, "Q130"); + assert_eq!(read("qubit q;\ndef f(qubit a) { f(a); }\nf(q);").unwrap_err().code, diag::TOO_DEEP); + assert!(diag::codes_markdown().contains("| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. |"), "Q209 as released"); +} + /// Programs nested `k` deep in each way the text counts, for the nesting bound's tests: /// each is deepest at 2 + `k` (a statement at 1, what it holds at 2, and `k` levels more). pub(crate) fn nested(shape: &str, k: usize) -> String { diff --git a/wai/wai-quantum/src/qasm/parse.rs b/wai/wai-quantum/src/qasm/parse.rs index cfbdfa3c6..0a10650d4 100644 --- a/wai/wai-quantum/src/qasm/parse.rs +++ b/wai/wai-quantum/src/qasm/parse.rs @@ -96,6 +96,39 @@ impl<'a> Parser<'a> { /// How many of its tokens have been read: a reader of another grammar /// that borrows these expressions (the pulse reader) knows where one /// ends. + /// A parser at token `pos` (the reader's callouts read an `extern`'s signature + /// from its statement, `qasm::read_callouts`). + pub(crate) fn at(src: &'a str, toks: &'a [Token], pos: usize) -> Self { + let mut p = Parser::new(src, toks); + p.pos = pos; + p + } + + /// At an `extern` statement: its argument types and its result type, when its + /// signature reads as a list of types; `None` when it does not. + pub(crate) fn extern_signature(&mut self) -> R, Option)>> { + if !self.eat_word("extern") { + return Ok(None); + } + self.name()?; + self.expect("(")?; + let mut args = Vec::new(); + if !self.eat(")") { + loop { + let Ok(t) = self.ty() else { return Ok(None) }; + args.push(t); + if self.eat(")") { + break; + } + if !self.eat(",") { + return Ok(None); + } + } + } + let ret = if self.eat("->") { Some(self.ty()?) } else { None }; + Ok(Some((args, ret))) + } + pub(crate) fn read_so_far(&self) -> usize { self.pos } diff --git a/wai/wai-quantum/src/quantum_tableau.rs b/wai/wai-quantum/src/quantum_tableau.rs index 67b973cdc..b223c0fa5 100644 --- a/wai/wai-quantum/src/quantum_tableau.rs +++ b/wai/wai-quantum/src/quantum_tableau.rs @@ -110,6 +110,11 @@ fn words(bits: usize) -> usize { bits.div_ceil(64) } +/// The lowest `k` bits of a word, `k` at most 64. +fn low_bits(k: usize) -> u64 { + if k >= 64 { u64::MAX } else { (1u64 << k) - 1 } +} + fn get(v: &[u64], i: usize) -> bool { v[i / 64] >> (i % 64) & 1 == 1 } @@ -474,7 +479,7 @@ impl Tableau { let col: Vec = self.xs[a * rw..(a + 1) * rw].to_vec(); let Some(p) = first_set(&col, n, 2 * n) else { // Z_a, up to sign, is a product of the stabilizers. - return Ok((self.expectation(&[(a as u32, 'Z')])? < 0, false)); + return Ok((self.z_negative(a), false)); }; let outcome = random(); // Every other row whose X part holds qubit a: row ← row p · row. @@ -542,6 +547,70 @@ impl Tableau { Ok((outcome, true)) } + /// Whether `Z_a`, when it is ± a product of the stabilizers, is minus it: the outcome of + /// a measurement of qubit `a` whose outcome is determined. The stabilizers it is the + /// product of are those of the destabilizers that hold X or Y at `a`. Their product's + /// letters are `Z_a`'s, so only its sign is wanted: twice the sum of their signs, and + /// for each qubit the powers of i its letters carry as they are multiplied in row + /// order (Aaronson and Gottesman's g), all rows of a word at once. Every order gives + /// the same product, since the stabilizers commute. This reads each of the `n` + /// qubits' run of stabilizer rows once, and holds nothing more than a run. + fn z_negative(&self, a: usize) -> bool { + let (n, rw) = (self.n, self.rw); + // The stabilizer rows in the product: row n + i for each destabilizer i with X at a. + let mut sel = vec![0u64; rw]; + let col = &self.xs[a * rw..(a + 1) * rw]; + for (w, &word) in col.iter().enumerate() { + let mut word = if w * 64 + 64 > n { word & low_bits(n.saturating_sub(w * 64)) } else { word }; + while word != 0 { + let i = w * 64 + word.trailing_zeros() as usize; + word &= word - 1; + set(&mut sel, n + i); + } + } + // Powers of i, mod 4. + let mut e: u64 = 0; + for (r, m) in self.rs.iter().zip(&sel) { + e += 2 * u64::from((r & m).count_ones()); + } + // Each row's prefix within its word: the XOR of the letters of the rows below it. + fn below(mut v: u64) -> u64 { + v ^= v << 1; + v ^= v << 2; + v ^= v << 4; + v ^= v << 8; + v ^= v << 16; + v ^= v << 32; + v + } + let first = n / 64; + for q in 0..n { + let (xc, zc) = (&self.xs[q * rw..(q + 1) * rw], &self.zs[q * rw..(q + 1) * rw]); + let (mut cx, mut cz) = (0u64, 0u64); + for w in first..rw { + let m = sel[w]; + if m == 0 { + continue; + } + let (x, z) = (xc[w] & m, zc[w] & m); + // The accumulated letter before each row: its prefix, and the words before. + let (px, pz) = (below(x) ^ x ^ cx, below(z) ^ z ^ cz); + if (x.count_ones() & 1) == 1 { + cx = !cx; + } + if (z.count_ones() & 1) == 1 { + cz = !cz; + } + // g(acc, letter): +1 or -1 (3 mod 4), as Aaronson and Gottesman give it. + let plus = (px & pz & z & !x) | (px & !pz & z & x) | (!px & pz & x & !z); + let minus = (px & pz & x & !z) | (px & !pz & z & !x) | (!px & pz & x & z); + e += u64::from(plus.count_ones()) + 3 * u64::from(minus.count_ones()); + } + } + debug_assert!(e.is_multiple_of(2), "a product of commuting stabilizers has a real sign"); + e % 4 == 2 + } + /// Rows `from..to` of the tableau, held row by row. fn rows(&self, from: usize, to: usize) -> Rows { let (n, rw) = (self.n, self.rw); @@ -1456,6 +1525,49 @@ mod tests { .collect() } + #[test] + fn a_determined_measurement_has_the_sign_of_the_whole_product() { + // `measure` takes a determined outcome from the stabilizers' product directly; it is + // the sign `expectation` finds by multiplying rows. Gates and measurements are + // interleaved, so that the generators are products of one another, with X and Y in + // them; then every qubit whose outcome is determined is checked, on sizes around a + // word's edges. The test counts the outcomes that take several rows, and several + // words of rows, so that it can fail for either. + let (mut checked, mut several, mut across) = (0, 0, 0); + for (n, seed) in [(2u32, 2u64), (3, 1), (5, 3), (63, 4), (64, 5), (65, 6), (127, 7), (128, 8), (130, 9), (200, 10)] { + for round in 0..12u64 { + let mut t = Tableau::new(n).unwrap(); + let mut s = seed * 7919 + round; + for _ in 0..12 { + for g in random_clifford(n, 4 * n as usize, next(&mut s)) { + t.apply(g).unwrap(); + } + for _ in 0..n.div_ceil(4) { + let q = (next(&mut s) % u64::from(n)) as u32; + let one = next(&mut s) & 1 == 1; + t.measure(q, || one).unwrap(); + } + } + for a in 0..n as usize { + let col = &t.xs[a * t.rw..(a + 1) * t.rw]; + if first_set(col, t.n, 2 * t.n).is_some() { + continue; + } + assert_eq!(t.z_negative(a), t.expectation(&[(a as u32, 'Z')]).unwrap() < 0, "n {n}, round {round}, qubit {a}"); + checked += 1; + let rows: Vec = (0..t.n).filter(|&i| get(col, i)).collect(); + if rows.len() > 1 { + several += 1; + if (t.n + rows[0]) / 64 != (t.n + rows[rows.len() - 1]) / 64 { + across += 1; + } + } + } + } + } + assert!(checked > 2000 && several > 200 && across > 80, "{checked} determined outcomes checked, {several} of several rows, {across} across words"); + } + #[test] fn the_zero_state_and_a_wide_ghz_state_read_exactly() { let t = Tableau::new(5).unwrap(); diff --git a/wai/wai-quantum/tests/wasmq_verdict.rs b/wai/wai-quantum/tests/wasmq_verdict.rs new file mode 100644 index 000000000..eb1afddaf --- /dev/null +++ b/wai/wai-quantum/tests/wasmq_verdict.rs @@ -0,0 +1,72 @@ +//! A wasmQ job on the one path every surface checks a job on (`engine::job`): the receipts +//! corpus's wasmQ jobs (`wai/quantum-conformance/receipts/`), read from their text and +//! parsed, each given the reasons its manifest writes by hand from the provider protocol's +//! §8.4 and quantum-wasmq §6.5, and the verdict's statement of the run beside them. +#![cfg(feature = "quantum_engine")] + +use serde_json::{Value, json}; +use wai_quantum::engine::job; + +fn corpus() -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/receipts") +} + +/// The manifest's wasmQ entries: each file's text and the reasons it must be given. +fn wasmq_entries() -> Vec<(String, String, Vec)> { + let manifest: Value = serde_json::from_slice(&std::fs::read(corpus().join("manifest.json")).unwrap()).unwrap(); + let out: Vec<_> = manifest["entries"] + .as_array() + .unwrap() + .iter() + .filter(|e| e["file"].as_str().unwrap().contains("wasmq")) + .map(|e| { + let name = e["file"].as_str().unwrap().to_string(); + let text = std::fs::read_to_string(corpus().join(&name)).unwrap(); + let mut want: Vec = e["reasons"].as_array().unwrap().iter().map(|r| r.as_str().unwrap().to_string()).collect(); + want.sort(); + (name, text, want) + }) + .collect(); + // Six made and a crafted one for each of several rules: a corpus with none tests nothing. + assert!(out.len() >= 12 && out.iter().filter(|(_, _, w)| w == &["wasmq_form"]).count() >= 6, "{}", out.len()); + out +} + +fn sorted(v: &Value) -> Vec { + let mut r: Vec = v.as_array().unwrap_or_else(|| panic!("no reasons: {v}")).iter().map(|r| r.as_str().unwrap().to_string()).collect(); + r.sort(); + r +} + +#[test] +fn every_reader_of_a_job_gives_a_wasmq_job_its_manifest_s_verdict() { + for (name, text, want) in wasmq_entries() { + let c = job::check_text(&text); + assert_eq!(sorted(&c.to_json()["reasons"]), want, "{name}: check_text"); + let v = job::verdict_of_text(&text).unwrap(); + assert_eq!((sorted(&v["reasons"]), v["valid"].as_bool()), (want.clone(), Some(want.is_empty())), "{name}: verdict_of_text"); + let parsed: Value = serde_json::from_str(&text).unwrap(); + let p = job::verdict(&parsed).unwrap(); + assert_eq!(sorted(&p["reasons"]), want, "{name}: verdict of the parsed job"); + assert_eq!(job::verify(&parsed).unwrap().0, want.is_empty(), "{name}: verify"); + } +} + +#[test] +fn a_wasmq_verdict_states_the_run_its_problems_and_the_host_s_words() { + let read = |name: &str| std::fs::read_to_string(corpus().join("jobs").join(name)).unwrap(); + let v = job::verdict_of_text(&read("cli-wasmq-fault.json")).unwrap(); + assert_eq!(v["wasmq"], json!({ "outcome": "fault", "problems": [], "notes": [] }), "{v}"); + // A rule broken under a seal that holds: the reason, and the rule in words. + let v = job::verdict_of_text(&read("crafted-wasmq-counts-short.json")).unwrap(); + assert_eq!(v["reasons"], json!(["wasmq_form"]), "{v}"); + assert!(v["wasmq"]["problems"][0].as_str().unwrap().starts_with("rule 19: counts sum to"), "{v}"); + // The host's unsealed words: noted, and no reason. + let v = job::verdict_of_text(&read("crafted-wasmq-envelope-changed.json")).unwrap(); + assert_eq!((v["valid"].as_bool(), v["wasmq"]["outcome"].as_str()), (Some(true), Some("fault")), "{v}"); + let notes = v["wasmq"]["notes"].as_array().unwrap(); + assert!(notes.iter().any(|n| n.as_str().unwrap().contains("unsealed status says \"done\"")) && notes.len() == 3, "{v}"); + // A job of another kind carries no statement of a run it is not. + let v = job::verdict_of_text(&read("cli-run-statevector.json")).unwrap(); + assert!(v.get("wasmq").is_none(), "{v}"); +} diff --git a/wai/wai-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index a82a689f2..9b05cbda1 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -109,6 +109,7 @@ const GLOSSARY_NO_ENTRY: &[(&str, &str)] = &[ (CAP_NEURAL_INT_SYNTH, WAI_NATIVE), (CAP_QUANTUM_CIRCUIT, WAI_NATIVE), (CAP_QUANTUM_CIRCUIT2, WAI_NATIVE), + (CAP_QUANTUM_WASMQ, WAI_NATIVE), (CAP_SPLAT_4D, WAI_NATIVE), (CAP_SPLAT_INT_CODEC, WAI_NATIVE), (CAP_TERNARY_TT, WAI_NATIVE), diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index 11dddb857..d60d66041 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -412,6 +412,12 @@ pub const CAP_QUANTUM_CIRCUIT: &str = "wai.quantum.circuit"; // behind the `quantum` feature. pub const CAP_QUANTUM_CIRCUIT2: &str = "wai.quantum.circuit2"; +// Hybrid quantum-classical execution (extensions/quantum-wasmq): an OpenQASM 3 program and +// the WebAssembly modules its `extern`s call, in one envelope, run with the same result +// bytes and the same fuel on every host and sealed in a job receipt. The runner is the +// wai-wasmq crate (the wai-quantum command line's `run`); this crate registers the string. +pub const CAP_QUANTUM_WASMQ: &str = "wai.quantum.wasmq"; + // Balanced-ternary matrix-product-state transform (extensions/ternary-tt): a // quantum-INSPIRED (tensor-network) LOSSY compressor whose whole decode is // integer (per-column integer scale, i64/i128), so the reconstruction is @@ -838,6 +844,8 @@ pub fn known_capabilities() -> &'static [&'static str] { CAP_AVATAR_POSE, CAP_SPLAT_4D, CAP_FILM_LINEAR, CAP_FILM_REEL, // deterministic quantum-circuit simulation (byte-exact statevector) CAP_QUANTUM_CIRCUIT, CAP_QUANTUM_CIRCUIT2, + // hybrid quantum-classical execution (a program and its WebAssembly callouts) + CAP_QUANTUM_WASMQ, // quantum-inspired ternary-MPS transform (byte-exact recon, lossy) CAP_TERNARY_TT, // neural-network coding: the prior artifact form + the general-tensor candidate @@ -1102,7 +1110,7 @@ pub fn capability_tier(cap: &str) -> Option { // fixed-point approximation of the ideal unitary evolution, but its // reconstruction is exact and portable; the honest note lives in the // extension's §Fidelity). -CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_AVATAR_FACE | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 => Lossless, +CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_AVATAR_FACE | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_QUANTUM_WASMQ => Lossless, // Splat delivery is byte-exact and the temporal reconstruction is // frame-state-exact; the splat representation itself approximates a // scene, so the honest tier matches the captured medium, not the @@ -1289,7 +1297,7 @@ pub fn capability_license(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT | CAP_PRIOR_SET + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_QUANTUM_WASMQ | CAP_TERNARY_TT | CAP_PRIOR_SET | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE // Tensor-file formats published with their reference implementations under // permissive licences (the safetensors layout under Apache-2.0, GGUF under MIT). @@ -1699,7 +1707,7 @@ pub fn capability_replicate_class(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_QUANTUM_WASMQ | CAP_TERNARY_TT // A parameter-set listing: SPEC §3.1 fixes its bytes exactly, and they are // its own output. A tensor file: its format fixes where each tensor's bytes are, // and reading hands them over unchanged (prior-carriage §9). A sealed set: opening @@ -2961,6 +2969,7 @@ mod tests { (CAP_FILM_REEL, "extensions/linear-film.md: reel-equivalence"), (CAP_QUANTUM_CIRCUIT, "extensions/quantum-sim.md: statevector-equivalence"), (CAP_QUANTUM_CIRCUIT2, "extensions/quantum-sim.md: statevector-equivalence"), + (CAP_QUANTUM_WASMQ, "extensions/quantum-wasmq.md §6.4: result-equivalence, the same result bytes and fuel on every host, over a stabilizer tableau and an integer stream of outcomes"), (CAP_TERNARY_TT, "extensions/ternary-tt.md: reconstruction-equivalence"), (CAP_PRIOR_SET, "SPEC.md §3.1: a listing's bytes are fixed exactly and are their own output"), (CAP_PRIOR_SAFETENSORS, "extensions/prior-carriage.md §9.1: the tensors' bytes are the file's, fixed by its header"), diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 09a384928..572b5edcd 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -369,6 +369,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_TERNARY_TT, S_INTERACTIVE, None, "2026-07-30"), (CAP_QUANTUM_CIRCUIT, S_INTERACTIVE, None, "2026-07-25"), (CAP_QUANTUM_CIRCUIT2, S_INTERACTIVE, None, "2026-10-09"), + (CAP_QUANTUM_WASMQ, S_INTERACTIVE, None, "2026-11-05"), (CAP_ASSET_PROVENANCE, S_INTERACTIVE, None, "2026-06-13"), (CAP_ASSET_C2PA, S_INTERACTIVE, None, "2026-11-09"), (CAP_QUANTUM_CALIBRATION, S_INTERACTIVE, None, "2026-08-01"), @@ -1147,6 +1148,7 @@ pub const UNREGISTERED_LABELS: &[UnregisteredLabel] = &[ // The revision a circuit contract names in its `ext` member (quantum-sim §2.1). label("wai.quantum.circuit/1", "a contract revision: the `ext` of a wai.quantum.circuit payload's contract (quantum-sim §2.1), a part of the capability's definition"), label("wai.quantum.circuit2/1", "a contract revision: the `ext` of a wai.quantum.circuit2 payload's contract (quantum-sim §2.1), a part of the capability's definition"), + label("wai.quantum.wasmq/1", "a contract revision: the `profile` of a wai.quantum.wasmq payload's contract and of its run's result (quantum-wasmq §2.2, §6.4, §10), a part of the capability's definition"), // Documents the quantum crates write that have no schema file. label("wai.quantum.job-record/1", "a document with no schema file: the record a job service's store keeps a job in (wai-quantum-service)"), label("wai.quantum.provider-job/1", "a document with no schema file: the record wai-quantum-qpu keeps of a job on a person's provider, written before the job is sent"), @@ -1155,6 +1157,7 @@ pub const UNREGISTERED_LABELS: &[UnregisteredLabel] = &[ label("wai.quantum.docs-manifest/1", "a document with no schema file: the list of documents tools/quantum-docs checks"), label("wai.quantum.write/1", "a document with no schema file: the transcript wai-quantum-writer keeps of writing a program for a task"), label("wai.quantum.model/1", "a document with no schema file: the manifest of a writer's model, the file wai-model.json (wai-quantum-writer's `coder`)"), + label("wai.quantum.wasmq.result/1", "a document with no schema file: the result of a wasmQ run, held to its form by wai-quantum's `engine::wasmq` (quantum-wasmq §6.4, §6.5)"), // The domains a digest starts with. label("wai.quantum.job-key/1", "a digest domain: the digest a job service keeps a client's idempotency key under, the client's name beside the key (wai-quantum-service)"), label("wai.quantum.trace/1", "a digest domain: an optimisation trace's first link is BLAKE3 of it and the request's canonical text (wai-quantum's `engine::variational`)"), @@ -1167,6 +1170,9 @@ pub const UNREGISTERED_LABELS: &[UnregisteredLabel] = &[ label("wai.quantum.provider/2", "refused on purpose: a version of the provider protocol that no text defines; the reference provider's test sends it to see it answered unsupported_version (quantum-providers §10)"), label("wai.quantum.provider/9", "refused on purpose: a version of the provider protocol that no text defines; the protocol schema's test sends it to see it answered unsupported_version (quantum-providers §10)"), label("wai.quantum.provider/999", "refused on purpose: a version of the provider protocol that no text defines; the provider conformance corpus sends it to see it answered unsupported_version (quantum-providers §10)"), + label("wai.quantum.wasmq/2", "refused on purpose: a wasmQ profile that no text defines; wasmQ's tests, the receipts corpus and its verifiers put it in a contract or a result to see the job refused (quantum-wasmq §6.5, rules 4 and 11)"), + label("wai.quantum.wasmq/1x", "refused on purpose: the wasmQ profile one letter longer; the receipts' differential check puts it in a contract to see both verifiers refuse the job (quantum-wasmq §6.5, rule 4)"), + label("wai.quantum.wasmx", "refused on purpose: not a capability; wasmQ's adversarial test and its vectors put it in a payload's manifest to see the envelope refused with W013 (quantum-wasmq §2.3)"), ]; /// One registered string with every property the registry gives it. @@ -1324,7 +1330,7 @@ pub fn capability_media(cap: &str) -> Option { CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_AVATAR_FACE => Interactive, CAP_PRIOR_NNC | CAP_PRIOR_SET | CAP_PRIOR_SAFETENSORS | CAP_PRIOR_GGUF | CAP_PRIOR_SEALED | CAP_PRIOR_TREE | CAP_TENSOR_NNC | CAP_TENSOR_WEFT | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 - | CAP_TERNARY_TT => Binary, + | CAP_QUANTUM_WASMQ | CAP_TERNARY_TT => Binary, c if c.starts_with("wai.image.") => Image, c if c.starts_with("wai.audio.") => Audio, c if c.starts_with("wai.video.") => Video, diff --git a/wai/wai-wasmq/Cargo.lock b/wai/wai-wasmq/Cargo.lock new file mode 100644 index 000000000..20ef3d52e --- /dev/null +++ b/wai/wai-wasmq/Cargo.lock @@ -0,0 +1,1342 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80552083a2db143c5d951debe9f59a7d9f22084c70abe450fed5220060fa26c3" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf84b7e5f0aa42cb0d55cabf01c520fdb70a50d8b572a841ee875083589d643" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d47ec2358cdbace0520ac212c003656b529f0c60e43fbbe9a331632cd9e8fc" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebb94b54db96b26e387bc8c698823d83d9254d551a6ed846a60cebf62ed7135" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be81b89cb7b186a26cf2014c312aeb17737da988b4444d16a0cb3424d637d44e" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb1b64a509c3814932fb9845ce8da835cd10d62c6c56dacfdcd9af389d3bf313" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e667a0e9ed3bbbed0e7e52cd2dea85a645745b39f547097e2fca0bf317cac9df" + +[[package]] +name = "cranelift-control" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fa7d5123705226a368b6b1c7b10f23036c35e8041ffdd5f6505790f5c31404" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bffb83ac1d7052691d1710ca8eb334d4fcaad363e589f2b89d38d97bda5776" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc727c856bf57eae76c72ac4f9b1eaf49947a2b02f699798262fc8399557b7a" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e743f35915d55cd3d3178aab483661f78c2f9274aa704f29be69e3789c5bd" + +[[package]] +name = "cranelift-native" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127c50e5e2b2b97116d4e2c079db6ce29b07303cd1ab48a8bb4c2abbf16a7a68" +dependencies = [ + "cranelift-codegen", + "libc", + "mach2", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.137.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f34b169b8cbbaed2b4995205f1d02d7eac4b2771df5479255fe5483e1334a39" + +[[package]] +name = "crc32fast" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c66a82a3d48db9b7c2ba1307e06fcb81a35ba82952f58bc1bfb218321f7c0b" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138a9a5997d9cba5e54c7c3b836d7164f2bfe0852c7e98d5835e1708d6efdf36" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" +dependencies = [ + "serde", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wai-quantum" +version = "0.9.0" +dependencies = [ + "blake3", + "ed25519-dalek", + "serde", + "serde_json", +] + +[[package]] +name = "wai-wasmq" +version = "0.1.0" +dependencies = [ + "blake3", + "serde_json", + "wai-quantum", + "wasmparser 0.261.0", + "wasmq-atom-planner", + "wasmq-qldpc-decoder", + "wasmtime", + "wat", + "wit-parser", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b883a26aea02eaabe2d2c3fba7eb039ef5cdcb5d5201ae5551e73d5e251a81f" +dependencies = [ + "leb128fmt", + "wasmparser 0.263.0", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fa0a054ae153606df820eb0a84213e28cda41bbe04ba2628e71b6ba100fcb" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a21c9e3325655ba02de3e645bc8357a8b3651a2e3ada25b6dbc37560deb9b7" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.261.0", +] + +[[package]] +name = "wasmq-atom-planner" +version = "0.1.0" +dependencies = [ + "wai-quantum", +] + +[[package]] +name = "wasmq-qldpc-decoder" +version = "0.1.0" +dependencies = [ + "wai-quantum", +] + +[[package]] +name = "wasmtime" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdfacce893df54d319b29720e79edb001cd1b48faaf78fdb3b9c608eaeeea29f" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.261.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-environ" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79df81f2a5d7a1cb8b9eb35b4a2d7a8cb8e948bd3e5a1f79036c0094879275a" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.261.0", + "wasmparser 0.261.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c2836b127721e0abdc3490545cdc6d5269d1050a19a4057909901c16e1b781" + +[[package]] +name = "wasmtime-internal-core" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765a08b721bf2b2e5cbd3e9365a71b497d57fc5e8b2cf73ddf12d85d502d7fa" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e8f190dd62c021d278ab0bd4afab06426844831d6a35dc369835754ad997d4" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.261.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6326d47c759c0478489a034f1282a6f516ce97594f9af785539697d03add17ec" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b78d046468d9aaf81e97954165369693084f43e313daad47f959b1169d596d3" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e5f3cb3fdc12489163dc60741696dddb32c9ed0662ff59c8dfb177ffd7a07" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf9357720c1477c43f88dace11923737e8aa87abe5f46f90e23c93a2a0fb3fe" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "50.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce7c663ad018cc86ce824e21b35b8bed9877ee9ca6af8ef8672e28852450392e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "wast" +version = "263.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d90bcf581b501aa1c358bc5dc0faa979cd375796908860ebcb6d1d70b907c7" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.263.0", +] + +[[package]] +name = "wat" +version = "1.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc9c5aca783b3f1838be89f3f9937c528b90ec5faa6fb62d809726c16a7d8f7" +dependencies = [ + "wast", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-parser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962e43c4db2da607b50839a531e5b63473268b40efdf8028d5c54b572ca4f6f6" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.261.0", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/wai/wai-wasmq/Cargo.toml b/wai/wai-wasmq/Cargo.toml new file mode 100644 index 000000000..55427a808 --- /dev/null +++ b/wai/wai-wasmq/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "wai-wasmq" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "wasmQ: a deterministic, metered, receipted WebAssembly profile for hybrid quantum-classical programs. An OpenQASM 3 program and the WebAssembly its `extern` calls run travel in one content-addressed WAI envelope, run with the same bytes and the same fuel on every host, and seal a signed receipt." +repository = "https://git.transaction.science/Transaction-Science/open-standards" +homepage = "https://wai.transaction.science/quantum-run" +keywords = ["quantum", "webassembly", "deterministic", "openqasm", "receipts"] +categories = ["science", "wasm", "simulation"] +readme = "README.md" + +[lib] +name = "wai_wasmq" +path = "src/lib.rs" + +[[bin]] +name = "wasmq_vectors" +path = "src/bin/wasmq_vectors.rs" + +[features] +default = ["wasmtime"] +# The native engine, one of the two the crate runs a module on: the pinned +# release of wasmtime, compiling with Cranelift. The interpreter (src/interp.rs) +# is always there; on wasm32 it is the only engine. +wasmtime = ["dep:wasmtime"] + +[dependencies] +blake3 = "1" +serde_json = { version = "1", features = ["float_roundtrip"] } +wai-quantum = { version = "0.9.0", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt"] } +wasmparser = { version = "0.261", default-features = false, features = ["std", "validate", "features", "simd"] } + +# scripts/check-wasmtime-pin.py holds this to the release every workflow runs. +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wasmtime = { version = "=50.0.1", optional = true, default-features = false, features = ["cranelift", "runtime", "std"] } + +[dev-dependencies] +wat = "1" +# The WIT file (extensions/quantum-wasmq.wit), read so a test holds the modules' exports +# and the callouts' lowering to it. +wit-parser = "0.261" +# The reference modules' own code, compiled natively: the tests hold each module's results +# equal to it. +wasmq-qldpc-decoder = { path = "modules/qldpc-decoder", default-features = false } +wasmq-atom-planner = { path = "modules/atom-planner", default-features = false } +wai-quantum = { version = "0.9.0", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_qec", "quantum_atom"] } diff --git a/wai/wai-wasmq/README.md b/wai/wai-wasmq/README.md new file mode 100644 index 000000000..5e5ae9724 --- /dev/null +++ b/wai/wai-wasmq/README.md @@ -0,0 +1,42 @@ +# wai-wasmq + +wasmQ: a deterministic, metered, receipted WebAssembly profile for hybrid quantum-classical +programs ([`../extensions/quantum-wasmq.md`](../extensions/quantum-wasmq.md)). + +An OpenQASM 3 program declares the classical functions it calls between gates with +`extern`. A wasmQ envelope carries the program, the WebAssembly modules that define those +functions and their priors, pinned by BLAKE3. This crate: + +- holds a module to the profile (`profile`): Lime1, nothing imported, memory, size and + stack bounded, each rule a refusal code; and reads a program within bounds it is held to + as it is read, so that no short program makes a host hold or do more than it has; +- prepares it (`meter`) so that it meters its own fuel, weighs its own stack and returns + only canonical NaNs, for an engine that does not hold those rules itself; +- runs it on two engines (`engine`): the pinned wasmtime release, and an interpreter of its + own (`interp`) that also runs on wasm32; +- packs and opens envelopes (`envelope`), bounding from the bundle alone what a run asks of + its host that fuel does not count, runs a bundle of Clifford gates shot by shot on + a stabilizer tableau with each callout metered (`run`), in whole numbers throughout, and + seals the job receipt every verifier of a job checks: the engine holds a wasmQ job to its + form (`wai-quantum`, `engine::wasmq`), and this crate takes the profile's constants from + there, so a run writes what a verifier holds it to; +- replays a record of calls through the modules alone (`run::replay`), and gives its digest + (`run::transcript_blake3`); +- builds the reference programs (`reference`) and checks the conformance vectors + (`vectors`, and the `wasmq_vectors` binary). + +The reference modules, the engine's qLDPC decoder and atom rearrangement planner built as +wasmQ modules, are in `modules/`; their modules of record are in +`../quantum-conformance/wasmq/modules/`. + +```sh +wai-quantum pack-wasmq memory.qasm --callout decode64=qldpc_decoder.wasm \ + --prior qldpc_decoder.wasm=checks.bin --shots 1000 -o memory.wai +wai-quantum run memory.wai --json > job.json +wai-quantum verify job.json --bundle memory.wai +cargo run --release --bin wasmq_vectors -- verify ../quantum-conformance/wasmq +``` + +`mutations.py` removes each of the crate's checks in turn, and says which test then fails: +a check whose tests all still pass is one no test is of. It builds the crate once for each, +so it runs on a build host, not in CI. diff --git a/wai/wai-wasmq/modules/.cargo/config.toml b/wai/wai-wasmq/modules/.cargo/config.toml new file mode 100644 index 000000000..d046fe0b9 --- /dev/null +++ b/wai/wai-wasmq/modules/.cargo/config.toml @@ -0,0 +1,3 @@ +# The profile takes a memory with a maximum, and Lime1's features only. +[target.wasm32-unknown-unknown] +rustflags = ["-C", "target-cpu=lime1", "-C", "link-arg=--max-memory=67108864"] diff --git a/wai/wai-wasmq/modules/Cargo.lock b/wai/wai-wasmq/modules/Cargo.lock new file mode 100644 index 000000000..37fc5deb3 --- /dev/null +++ b/wai/wai-wasmq/modules/Cargo.lock @@ -0,0 +1,462 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wai-quantum" +version = "0.9.0" +dependencies = [ + "blake3", + "ed25519-dalek", + "serde_json", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasmq-atom-planner" +version = "0.1.0" +dependencies = [ + "wai-quantum", +] + +[[package]] +name = "wasmq-qldpc-decoder" +version = "0.1.0" +dependencies = [ + "wai-quantum", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/wai/wai-wasmq/modules/Cargo.toml b/wai/wai-wasmq/modules/Cargo.toml new file mode 100644 index 000000000..ea687ceac --- /dev/null +++ b/wai/wai-wasmq/modules/Cargo.toml @@ -0,0 +1,19 @@ +# The reference modules of wasmQ (extensions/quantum-wasmq.md §8): code the engine already +# has, built as wasmQ modules. `build.sh` builds them for wasm32-unknown-unknown and writes +# the modules of record into wai/quantum-conformance/wasmq/modules/. +[workspace] +resolver = "3" +members = ["qldpc-decoder", "atom-planner"] + +[workspace.package] +edition = "2024" +license = "Apache-2.0" +publish = false + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +debug = false +strip = true diff --git a/wai/wai-wasmq/modules/atom-planner/Cargo.toml b/wai/wai-wasmq/modules/atom-planner/Cargo.toml new file mode 100644 index 000000000..f854218fb --- /dev/null +++ b/wai/wai-wasmq/modules/atom-planner/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wasmq-atom-planner" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish.workspace = true +description = "The engine's neutral-atom rearrangement planner (wai_quantum::quantum_atom, an exact Hungarian assignment) as a wasmQ module." + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# The module's exports (`cabi_realloc`, `wasmq-prior`, its callouts) on wasm32. A host that +# links the crate's functions natively, or links both reference crates into one wasm32 +# test, turns it off. +default = ["exports"] +exports = [] + +[dependencies] +wai-quantum = { path = "../../../wai-quantum", default-features = false, features = ["quantum_atom"] } diff --git a/wai/wai-wasmq/modules/atom-planner/src/lib.rs b/wai/wai-wasmq/modules/atom-planner/src/lib.rs new file mode 100644 index 000000000..c4acfd806 --- /dev/null +++ b/wai/wai-wasmq/modules/atom-planner/src/lib.rs @@ -0,0 +1,116 @@ +//! The engine's neutral-atom rearrangement planner as a wasmQ module +//! (extensions/quantum-wasmq.md §8.2). +//! +//! Prior 0 is the target register in the form `quantum_atom::target_bytes` writes: the +//! array's width and height and the target sites. `plan` takes the array's occupancy, +//! one bit for each site (row-major), as a mid-circuit readout gives it, and returns the +//! plan `wai_quantum::quantum_atom::solve` makes as masks: a bit for each site an atom +//! leaves, a bit for each site an atom fills, and a last bit for whether the plan covers +//! every target site. `plan64` is the same call, in the scalar form, for an array of at +//! most 31 sites; `plan` is the list form, for any array. +//! +//! The same functions run natively in the tests (`wai-wasmq/tests/reference.rs`). + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +use std::cell::RefCell; +use wai_quantum::quantum_atom::{Array, solve}; + +/// The array's width and height and the target sites, from `target_bytes`. +pub fn read_target(b: &[u8]) -> Option<(u32, u32, Vec)> { + let rest = b.strip_prefix(b"wai:qc-target\x01".as_slice())?; + let u = |at: usize| -> Option { Some(u32::from_le_bytes(rest.get(at..at + 4)?.try_into().ok()?)) }; + let (w, h) = (u(0)?, u(4)?); + let mut sites = Vec::new(); + let mut at = 8; + while at < rest.len() { + let s = u(at)? as usize; + if s >= (w * h) as usize { + return None; + } + sites.push(s); + at += 4; + } + Some((w, h, sites)) +} + +/// The plan for the occupancy `occ`, as the masks `plan` returns: `2·w·h + 1` bits. +pub fn plan_bits(w: u32, h: u32, target: &[usize], occ: &[bool]) -> Vec { + let n = (w * h) as usize; + let a = Array { w, h, occ: occ.to_vec() }; + let p = solve(&a, target); + let mut out = vec![false; 2 * n + 1]; + if p.feasible { + for &(from, to) in &p.moves { + out[from] = true; + out[n + to] = true; + } + } + out[2 * n] = p.feasible; + out +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +struct State { + target: Option<(u32, u32, Vec)>, + ret: [u32; 2], + out: Vec, +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +thread_local! { + static S: RefCell = const { RefCell::new(State { target: None, ret: [0; 2], out: Vec::new() }) }; +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +mod exports { + use super::*; + use std::alloc::{Layout, alloc, realloc}; + + #[unsafe(export_name = "cabi_realloc")] + pub extern "C" fn cabi_realloc(old: *mut u8, old_len: usize, align: usize, len: usize) -> *mut u8 { + unsafe { + if old_len == 0 { + alloc(Layout::from_size_align_unchecked(len.max(1), align.max(1))) + } else { + realloc(old, Layout::from_size_align_unchecked(old_len, align.max(1)), len.max(1)) + } + } + } + + #[unsafe(export_name = "wasmq-prior")] + pub extern "C" fn prior(index: u32, ptr: *const u8, len: usize) { + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + if index == 0 { + let t = read_target(bytes).expect("prior 0 is the target register"); + S.with(|s| s.borrow_mut().target = Some(t)); + } + } + + #[unsafe(export_name = "plan")] + pub extern "C" fn plan(ptr: *const u8, len: usize) -> *const [u32; 2] { + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + S.with(|s| { + let mut s = s.borrow_mut(); + let (w, h, t) = s.target.clone().expect("plan before the target's prior"); + let occ: Vec = (0..(w * h) as usize).map(|i| bytes[i / 8] >> (i % 8) & 1 == 1).collect(); + let bits = plan_bits(w, h, &t, &occ); + let mut out = vec![0u8; bits.len().div_ceil(8)]; + for (i, &b) in bits.iter().enumerate() { + out[i / 8] |= (b as u8) << (i % 8); + } + s.out = out; + s.ret = [s.out.as_ptr() as u32, s.out.len() as u32]; + &s.ret as *const [u32; 2] + }) + } + + #[unsafe(export_name = "plan64")] + pub extern "C" fn plan64(occ: u64) -> u64 { + S.with(|s| { + let s = s.borrow(); + let (w, h, t) = s.target.as_ref().expect("plan64 before the target's prior"); + let o: Vec = (0..(w * h) as usize).map(|i| occ >> i & 1 == 1).collect(); + plan_bits(*w, *h, t, &o).iter().enumerate().fold(0u64, |acc, (i, &b)| acc | (b as u64) << i) + }) + } +} diff --git a/wai/wai-wasmq/modules/build.sh b/wai/wai-wasmq/modules/build.sh new file mode 100755 index 000000000..2c9156d57 --- /dev/null +++ b/wai/wai-wasmq/modules/build.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Builds the reference modules (extensions/quantum-wasmq.md §8) for wasm32-unknown-unknown +# and writes the modules of record, with what decided their bytes, into +# wai/quantum-conformance/wasmq/modules/. +# +# build.sh build and write the modules of record and modules.json +# build.sh --check build, and fail unless the build equals the modules of record +# build.sh --sources build nothing; fail unless every source modules.json pins has +# its digest (any host: python3 alone) +# +# rustc's wasm32 output depends on the host it runs on, so the modules of record are +# built on x86_64 Linux, the CI image's host; `--check` is run there. Every other host +# reads the committed modules, and modules.json pins the sources that made them. +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../../.." && pwd) +out="$root/wai/quantum-conformance/wasmq/modules" +check=0 +mkdir -p "$out" +[ "${1:-}" = "--check" ] && check=1 +if [ "${1:-}" = "--sources" ]; then + python3 -I - "$root" <<'PY' +import hashlib, json, os, sys +root = sys.argv[1] +doc = json.load(open(os.path.join(root, "wai/quantum-conformance/wasmq/modules/modules.json"))) +bad = [p for p, h in doc["sources_sha256"].items() if hashlib.sha256(open(os.path.join(root, p), "rb").read()).hexdigest() != h] +for n, m in doc["modules"].items(): + b = open(os.path.join(root, "wai/quantum-conformance/wasmq/modules", m["file"]), "rb").read() + if hashlib.sha256(b).hexdigest() != m["sha256"]: + bad.append(m["file"]) +for p in bad: + print(f"::error::{p} is not what the modules of record were built from: rebuild them with wai/wai-wasmq/modules/build.sh on x86_64 Linux") +print(f"modules of record: {len(doc['sources_sha256'])} sources and {len(doc['modules'])} modules as pinned" if not bad else "") +sys.exit(1 if bad else 0) +PY + exit $? +fi +cd "$here" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$here/target}" +sysroot=$(rustc --print sysroot) +commit=$(rustc -vV | sed -n 's/^commit-hash: //p') +# The paths a build records: the same on every host. +export RUSTFLAGS="-C target-cpu=lime1 -C link-arg=--max-memory=67108864 --remap-path-prefix=$root=/src --remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$sysroot/lib/rustlib/src/rust=/rustc/$commit" +cargo build --locked --release --target wasm32-unknown-unknown +built="$CARGO_TARGET_DIR/wasm32-unknown-unknown/release" +status=0 +for name in qldpc_decoder atom_planner; do + f="$built/wasmq_$name.wasm" + if [ "$check" = 1 ]; then + if cmp -s "$f" "$out/$name.wasm"; then + echo "$name: the build equals the module of record" + else + echo "::error::$name: the build differs from the module of record ($(wc -c <"$f") bytes against $(wc -c <"$out/$name.wasm"))" + status=1 + fi + else + cp "$f" "$out/$name.wasm" + fi +done +if [ "$check" = 0 ]; then + python3 - "$root" "$(rustc -V)" <<'PY' +import hashlib, json, sys, os +root, rustc = sys.argv[1], sys.argv[2] +def b3(p): + # blake3 is not in the standard library; sha256 pins the sources, the crate pins the modules by blake3. + return hashlib.sha256(open(os.path.join(root, p), 'rb').read()).hexdigest() +sources = ["wai/wai-wasmq/modules/Cargo.toml", "wai/wai-wasmq/modules/Cargo.lock", "wai/wai-wasmq/modules/.cargo/config.toml", + "wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml", "wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs", + "wai/wai-wasmq/modules/atom-planner/Cargo.toml", "wai/wai-wasmq/modules/atom-planner/src/lib.rs", + "wai/wai-quantum/src/quantum_qec.rs", "wai/wai-quantum/src/quantum_atom.rs", "wai/wai-quantum/src/quantum_ops.rs", "rust-toolchain.toml"] +mods = {} +for n in ["qldpc_decoder", "atom_planner"]: + b = open(os.path.join(root, "wai/quantum-conformance/wasmq/modules", n + ".wasm"), 'rb').read() + mods[n] = {"file": n + ".wasm", "bytes": len(b), "sha256": hashlib.sha256(b).hexdigest()} +doc = {"built_with": rustc, "target": "wasm32-unknown-unknown", "host": "x86_64-unknown-linux-gnu", "modules": mods, "sources_sha256": {s: b3(s) for s in sources}} +open(os.path.join(root, "wai/quantum-conformance/wasmq/modules/modules.json"), "w").write(json.dumps(doc, indent=2, sort_keys=True) + "\n") +PY + echo "wrote the modules of record and modules.json" +fi +exit $status diff --git a/wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml b/wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml new file mode 100644 index 000000000..876eab190 --- /dev/null +++ b/wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wasmq-qldpc-decoder" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish.workspace = true +description = "The engine's qLDPC decoder (wai_quantum::quantum_qec, Relay-BP in fixed point) as a wasmQ module." + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# The module's exports (`cabi_realloc`, `wasmq-prior`, its callouts) on wasm32. A host that +# links the crate's functions natively, or links both reference crates into one wasm32 +# test, turns it off. +default = ["exports"] +exports = [] + +[dependencies] +wai-quantum = { path = "../../../wai-quantum", default-features = false, features = ["quantum_qec"] } diff --git a/wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs b/wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs new file mode 100644 index 000000000..1802d8281 --- /dev/null +++ b/wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs @@ -0,0 +1,123 @@ +//! The engine's qLDPC decoder as a wasmQ module (extensions/quantum-wasmq.md §8.1). +//! +//! Prior 0 is the code's parity checks in the form `CssCode::check_matrix_bytes` writes. +//! `decode` takes a syndrome, one bit for each check, and returns a correction, one bit +//! for each qubit, chosen as the engine chooses a decoder (`quantum_qec`'s own account): +//! union-find matching (`union_find_correct`) for a code with a matching graph, every +//! qubit in at most two checks, such as the surface code; Relay-BP (`relay_bp_correct`, +//! 32 iterations a leg, 12 legs, the seed the run gives the instance) otherwise, such as +//! the bivariate-bicycle codes. `decode64` is the same call for a code of at most 64 +//! checks and 64 qubits, its bits in one integer. +//! +//! The same functions run natively in the tests (`wai-wasmq/tests/reference.rs`), which +//! hold every result of the module equal to the native one. + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +use std::cell::RefCell; +use wai_quantum::quantum_qec::{CssCode, DecodeConfig, relay_bp_correct, union_find_correct}; + +/// The decoder's configuration: 32 iterations a leg, 12 legs. +pub const CONFIG: DecodeConfig = DecodeConfig { p_fx: 0, max_iter: 32, max_legs: 12 }; + +/// A code's parity checks read from `CssCode::check_matrix_bytes`. +pub fn read_checks(b: &[u8]) -> Option { + let tag = b"wai:qec-checks\x01"; + let rest = b.strip_prefix(tag.as_slice())?; + let n = u64::from_le_bytes(rest.get(..8)?.try_into().ok()?) as usize; + let mut at = 8; + let mut checks = Vec::new(); + let u32_at = |at: usize| -> Option { Some(u32::from_le_bytes(rest.get(at..at + 4)?.try_into().ok()?)) }; + while at < rest.len() { + let len = u32_at(at)? as usize; + at += 4; + let mut row = Vec::with_capacity(len); + for _ in 0..len { + let c = u32_at(at)? as usize; + if c >= n { + return None; + } + row.push(c); + at += 4; + } + checks.push(row); + } + Some(CssCode::from_families("prior", n, checks, Vec::new())) +} + +/// The correction for `syndrome`: matching where the code has a matching graph, Relay-BP +/// where it does not. +pub fn decode_bits(code: &CssCode, syndrome: &[bool], seed: u64) -> Vec { + union_find_correct(code, syndrome).unwrap_or_else(|| relay_bp_correct(code, syndrome, &CONFIG, seed).0) +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +struct State { + seed: u64, + code: Option, + ret: [u32; 2], + out: Vec, +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +thread_local! { + static S: RefCell = const { RefCell::new(State { seed: 0, code: None, ret: [0; 2], out: Vec::new() }) }; +} + +#[cfg(all(target_arch = "wasm32", feature = "exports"))] +mod exports { + use super::*; + use std::alloc::{Layout, alloc, realloc}; + + #[unsafe(export_name = "cabi_realloc")] + pub extern "C" fn cabi_realloc(old: *mut u8, old_len: usize, align: usize, len: usize) -> *mut u8 { + unsafe { + if old_len == 0 { + alloc(Layout::from_size_align_unchecked(len.max(1), align.max(1))) + } else { + realloc(old, Layout::from_size_align_unchecked(old_len, align.max(1)), len.max(1)) + } + } + } + + #[unsafe(export_name = "wasmq-seed")] + pub extern "C" fn seed(seed: u64) { + S.with(|s| s.borrow_mut().seed = seed); + } + + #[unsafe(export_name = "wasmq-prior")] + pub extern "C" fn prior(index: u32, ptr: *const u8, len: usize) { + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + if index == 0 { + let code = read_checks(bytes).expect("prior 0 is a code's parity checks"); + S.with(|s| s.borrow_mut().code = Some(code)); + } + } + + #[unsafe(export_name = "decode")] + pub extern "C" fn decode(ptr: *const u8, len: usize) -> *const [u32; 2] { + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + S.with(|s| { + let mut s = s.borrow_mut(); + let code = s.code.as_ref().expect("decode before the code's prior"); + let syndrome: Vec = (0..code.checks.len()).map(|i| bytes[i / 8] >> (i % 8) & 1 == 1).collect(); + let c = decode_bits(code, &syndrome, s.seed); + let mut out = vec![0u8; c.len().div_ceil(8)]; + for (i, &b) in c.iter().enumerate() { + out[i / 8] |= (b as u8) << (i % 8); + } + s.out = out; + s.ret = [s.out.as_ptr() as u32, s.out.len() as u32]; + &s.ret as *const [u32; 2] + }) + } + + #[unsafe(export_name = "decode64")] + pub extern "C" fn decode64(syndrome: u64) -> u64 { + S.with(|s| { + let s = s.borrow(); + let code = s.code.as_ref().expect("decode64 before the code's prior"); + let syn: Vec = (0..code.checks.len()).map(|i| syndrome >> i & 1 == 1).collect(); + decode_bits(code, &syn, s.seed).iter().enumerate().fold(0u64, |acc, (i, &b)| acc | (b as u64) << i) + }) + } +} diff --git a/wai/wai-wasmq/mutations.py b/wai/wai-wasmq/mutations.py new file mode 100644 index 000000000..225d72d59 --- /dev/null +++ b/wai/wai-wasmq/mutations.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""The checks of wasmQ, each removed in turn: the test that is of it must then fail. + +A test that still passes with its check removed cannot fail for that check, and says +nothing of it. Each entry below names a check, where it is, how it is removed, and the +tests that must then fail. Run from the repository's root, on a build host (it builds +and tests a crate for each entry, so it takes a while): + + python3 wai/wai-wasmq/mutations.py every entry + python3 wai/wai-wasmq/mutations.py WORD ... the entries whose name holds a WORD + python3 wai/wai-wasmq/mutations.py --check only that each statement is in the code once + +Each file is restored after its entry, whatever happens. Exit 0 when every entry is +caught: its tests ran and at least one failed, or the test process died. +""" +import os +import subprocess +import sys + +WASMQ = "wai/wai-wasmq" +QUANTUM = "wai/wai-quantum" +CLI = "wai/wai-quantum-cli" + +# (name, the crate of the file, the file in it, the statement, what it becomes, the cargo +# test arguments, the tests, and optionally the crate whose tests they are). The tests are wasmQ's, but for a file of wai-quantum tested by +# that crate's own tests (`--lib`, or a test of its features), and the command line's for a +# file of the command line's. +M = [ + # The envelope (§2.3). + ("the payload's magic", WASMQ, "src/envelope.rs", " if take(payload, &mut p, 4)? != MAGIC {", " if take(payload, &mut p, 4)? != MAGIC && false {", ["--test", "adversarial"], ["each_rule_of_the_envelope"]), + ("the contract's canonical form", WASMQ, "src/envelope.rs", " if canonical(&contract) != text {", " if canonical(&contract) != text && false {", ["--test", "adversarial"], ["each_rule_of_the_envelope"]), + ("the frames end with the payload", WASMQ, "src/envelope.rs", " if p != payload.len() {", " if p != payload.len() && false {", ["--test", "adversarial"], ["each_rule_of_the_envelope"]), + ("the contract's members", WASMQ, "src/envelope.rs", " if canonical(&self::contract(&bundle, &method)) != text {", " if canonical(&self::contract(&bundle, &method)) != text && false {", ["--test", "vectors"], ["each_refusal_is_refused"]), + ("the manifest is the contract's", WASMQ, "src/envelope.rs", " if manifest_bytes != self::manifest(&bundle, h.reading.program.n_qubits)?.as_slice() {", " if manifest_bytes != self::manifest(&bundle, h.reading.program.n_qubits)?.as_slice() && false {", ["--test", "adversarial"], ["each_rule_of_the_envelope"]), + ("a frame's digest", WASMQ, "src/envelope.rs", " if got != want {", " if got != want && false {", ["--test", "adversarial"], ["a_changed_module_or_program", "a_prior_whose_hash"]), + ("an input a double holds", WASMQ, "src/envelope.rs", "v.unsigned_abs() > MAX_EXACT) {", "v.unsigned_abs() > MAX_EXACT && false) {", ["--test", "review"], ["an_input_a_double_cannot_hold"]), + ("one callout for each extern", WASMQ, "src/envelope.rs", " if b.callouts.len() != p.externs.len() || b.callouts.iter().zip(&p.externs).any(|(c, d)| c.name != d.name) {", " if false {", ["--test", "review"], ["a_contract_names_one_callout"]), + ("the memory a host reaches", WASMQ, "src/envelope.rs", " if f.memory.is_none() || !f.memory_exported {", " if f.memory.is_none() {", ["--test", "review"], ["a_bundle_whose_memory_the_host_cannot_reach"]), + ("a module with priors exports cabi_realloc", WASMQ, "src/envelope.rs", ' reachable(f, &m.name, "the host passes a prior", true)?;', ' reachable(f, &m.name, "the host passes a prior", false)?;', ["--test", "review"], ["a_module_with_priors_exports"]), + ("the Clifford gates", WASMQ, "src/envelope.rs", " if let Some(op) = p.ops.iter().find(", " if let Some(op) = p.ops.iter().take(0).find(", ["--test", "review"], ["a_program_outside_the_clifford_gates"]), + # What a run asks of its host (§4.7, §4.8). + ("an envelope's bytes", WASMQ, "src/envelope.rs", " if bytes.len() > MAX_ENVELOPE_BYTES {", " if bytes.len() > MAX_ENVELOPE_BYTES && false {", ["--test", "sweep"], ["an_envelope_past"]), + ("an envelope's frames", WASMQ, "src/envelope.rs", " if frames >= MAX_ENVELOPE_BYTES as u64 {", " if frames >= MAX_ENVELOPE_BYTES as u64 && false {", ["--test", "sweep"], ["an_envelope_past"]), + ("a program's text", WASMQ, "src/envelope.rs", " if program.len() > MAX_PROGRAM_BYTES {", " if program.len() > MAX_PROGRAM_BYTES && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the code among a bundle's modules", WASMQ, "src/envelope.rs", " if code > MAX_CODE_BYTES {", " if code > MAX_CODE_BYTES && false {", ["--test", "sweep"], ["the_code_among_a_bundle"]), + ("the memory among a bundle's modules", WASMQ, "src/envelope.rs", " if pages > profile::MAX_MEMORY_PAGES {", " if pages > profile::MAX_MEMORY_PAGES && false {", ["--test", "sweep"], ["the_modules_of_a_bundle"]), + ("a memory's maximum, not its start", WASMQ, "src/envelope.rs", ".map(|(_, maximum)| maximum).sum();", ".map(|(initial, _)| initial.min(1)).sum();", ["--test", "sweep"], ["the_modules_of_a_bundle"]), + ("a run's results", WASMQ, "src/envelope.rs", " if bits > MAX_RESULT_BITS {", " if bits > MAX_RESULT_BITS && false {", ["--test", "sweep"], ["a_bundle_whose_result"]), + ("a run's results, at the limit", WASMQ, "src/envelope.rs", " if bits > MAX_RESULT_BITS {", " if bits >= MAX_RESULT_BITS {", ["--test", "sweep"], ["a_bundle_whose_result"]), + ("a run's work", WASMQ, "src/envelope.rs", " if work > MAX_HOST_WORK {", " if work > MAX_HOST_WORK && false {", ["--test", "sweep"], ["a_run_no_host", "the_work_limit"]), + ("a run's work, at the limit", WASMQ, "src/envelope.rs", " if work > MAX_HOST_WORK {", " if work >= MAX_HOST_WORK {", ["--test", "sweep"], ["the_work_limit", "a_run_s_fuel_is_bounded"]), + ("a gate's work", WASMQ, "src/envelope.rs", " Step::Instr(Instr::Gate(_)) => r,", " Step::Instr(Instr::Gate(_)) => 1,", ["--test", "sweep"], ["the_work_a_run_asks"]), + ("a measurement's work", WASMQ, "src/envelope.rs", " Step::Instr(Instr::Measure { .. } | Instr::Reset(_)) => MEASURE_WORK * n * r,", " Step::Instr(Instr::Measure { .. } | Instr::Reset(_)) => n * r,", ["--test", "sweep"], ["the_work_a_run_asks", "a_run_no_host"]), + ("a tableau's work", WASMQ, "src/envelope.rs", " let mut shot = 2 * n * r;", " let mut shot = 0;", ["--test", "sweep"], ["the_work_a_run_asks"]), + ("the calls of a callout", WASMQ, "src/envelope.rs", " CALL_WORK * (1 + lists + u128::from(post && crate::abi::uses_memory(decl)))", " CALL_WORK", ["--test", "sweep"], ["a_callout_s_work"]), + ("the bits of a callout", WASMQ, "src/envelope.rs", " + BIT_WORK * (input.len() + output.len()) as u128 + 1", " + 1", ["--test", "sweep"], ["a_callout_s_work", "the_work_a_run_asks"]), + ("the calls that make an instance", WASMQ, "src/envelope.rs", " let calls = 1 + 2 * m.priors.len() + ", " let calls = 1 + m.priors.len() + ", ["--test", "sweep"], ["a_callout_s_work", "the_work_a_run_asks"]), + ("an instance's memory", WASMQ, "src/envelope.rs", ".map_or(0, |(initial, _)| u128::from(initial)) * 8192;", ".map_or(0, |(initial, _)| u128::from(initial));", ["--test", "sweep"], ["the_work_a_run_asks", "memory_a_module_grows"]), + ("one instance for a run of `job`", WASMQ, "src/envelope.rs", ' let instances = if b.run.state == "shot" { b.run.shots } else { 1 };', " let instances = b.run.shots;", ["--test", "sweep"], ["the_work_a_run_asks", "a_run_no_host"]), + ("a module's code in the work", WASMQ, "src/envelope.rs", " code += CODE_WORK * m.bytes.len() as u128;", "", ["--test", "sweep"], ["the_work_a_run_asks"]), + ("the fuel in the work", WASMQ, "src/envelope.rs", " + code + FUEL_WORK * u128::from(b.run.per_job)", " + code", ["--test", "sweep"], ["a_run_s_fuel_is_bounded"]), + # The reader's bounds (§4.8), and its nesting. + ("the reader's work", QUANTUM, "src/qasm/elab.rs", " if self.reading > self.bounds.reading {\n return self.fail(", " if self.reading > self.bounds.reading && false {\n return self.fail(", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the steps of a statement", QUANTUM, "src/qasm/elab.rs", " if self.steps.len().saturating_add(more) > self.bounds.steps {", " if self.steps.len().saturating_add(more) > self.bounds.steps && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the steps of a reading", QUANTUM, "src/qasm/elab.rs", " if steps > bounds.steps {", " if steps > bounds.steps && false {", ["--test", "sweep"], ["the_steps_of_a_program"]), + ("the bits of a program", QUANTUM, "src/qasm/elab.rs", " if held > u64::from(self.bounds.bits) {", " if held > u64::from(self.bounds.bits) && false {", ["--test", "sweep"], ["a_bundle_whose_result"]), + ("the bits of the calls", QUANTUM, "src/qasm/elab.rs", " if self.callout_bits > self.bounds.callout_bits {", " if self.callout_bits > self.bounds.callout_bits && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("an array element's bits", QUANTUM, "src/qasm/elab.rs", " Ty::Bit(n) => u64::from(n.unwrap_or(1)),", " Ty::Bit(_) => 1u64,", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("an array's elements", QUANTUM, "src/qasm/elab.rs", " if elements > u64::from(self.bounds.bits) {", " if elements > u64::from(self.bounds.bits) && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the qubits a program declares", QUANTUM, "src/qasm/elab.rs", " if total > u64::from(self.bounds.qubits) {", " if total > u64::from(self.bounds.qubits) && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the qubits a program names", QUANTUM, "src/qasm/elab.rs", " if m >= MAX_QUBITS || m >= self.bounds.qubits {", " if m >= MAX_QUBITS {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("the nesting", QUANTUM, "src/qasm/parse.rs", " if self.depth > super::MAX_NESTING {", " if self.depth > super::MAX_NESTING && false {", ["--test", "reading"], ["a_program_past_the_bounds"]), + ("no measurement added to a hybrid program", QUANTUM, "src/qasm/elab.rs", " if measured.is_empty() && !self.callouts {", " if measured.is_empty() {", ["--test", "vectors"], ["the_vectors_are"]), + # Lowering and the lifecycle (§5). + ("a callout's width", WASMQ, "src/abi.rs", " if n < 64 && v >> n != 0 {", " if n < 64 && v >> n != 0 && false {", ["--test", "adversarial"], ["a_trap_and_a_result_past_its_width"]), + ("a signed argument, sign-extended", WASMQ, "src/abi.rs", " && v >> (w - 1) & 1 == 1", " && false", ["--test", "vectors"], ["the_vectors_are"]), + ("a list result is 4-aligned", WASMQ, "src/abi.rs", " if !ret.is_multiple_of(4) {", " if false {", ["--test", "vectors"], ["the_vectors_are"]), + ("cabi_post after a list result", WASMQ, "src/abi.rs", " if has_post {", " if false {", ["--test", "vectors"], ["the_vectors_are"]), + # The engines (§4). + ("a body ends", WASMQ, "src/profile.rs", " ops.finish().map_err(malformed)?;", "", ["--test", "review"], ["a_body_that_does_not_end"]), + ("a frame's operands", WASMQ, "src/profile.rs", " operands = operands.max(fv.operand_stack_height());", "", ["--test", "engines"], ["a_native_frame"]), + ("a frame's operands, in the interpreter", WASMQ, "src/interp.rs", " operands = operands.max(fv.operand_stack_height());", "", ["--test", "engines"], ["a_native_frame"]), + ("the stack before an indirect call", WASMQ, "src/interp.rs", " self.suspend(f.weight)?;\n let i = self.pop() as u32 as usize;", " let i = self.pop() as u32 as usize;", ["--test", "review"], ["an_indirect_call_where_the_stack_ends"]), + ("the engine reaches only an exported memory", WASMQ, "src/interp.rs", " self.m.memory_exported.then_some(&mut self.mem[..])", " Some(&mut self.mem[..])", ["--test", "review"], ["every_engine_reaches_a_module_s_memory"]), + ("an argument is of its parameter's type", WASMQ, "src/interp.rs", "if matches!(t, ValType::I32 | ValType::F32) { a as u32 as u64 } else { a }", "if matches!(t, ValType::I32 | ValType::F32) { a } else { a }", ["--test", "review"], ["an_argument_is_read_as_a_value"]), + ("a NaN is made canonical by the interpreter", WASMQ, "src/interp.rs", "fn c64(x: f64) -> u64 {\n if x.is_nan() { CANON64 } else { x.to_bits() }", "fn c64(x: f64) -> u64 {\n if false { CANON64 } else { x.to_bits() }", ["--test", "fuzz"], ["a_changed_module_is_refused"]), + ("a page grown, in the interpreter's fuel", WASMQ, "src/interp.rs", " self.charge(delta * meter::PAGE_FUEL)?;", " self.charge(delta)?;", ["--test", "sweep"], ["memory_a_module_grows"]), + ("a page grown, in the prepared module's fuel", WASMQ, "src/meter.rs", "if matches!(op, Operator::MemoryGrow { .. }) { PAGE_FUEL } else { 1 }", "1", ["--test", "engines"], ["the_fuel_is_the_same"]), + ("a run's fuel is charged at its start", WASMQ, "src/meter.rs", " charge_const(&mut f, &g, cost);", "", ["--test", "fuzz"], ["a_changed_module_is_refused"]), + ("the frame weight in the prepared module", WASMQ, "src/meter.rs", " stack_in(&mut f, &g, weight_here);", " stack_in(&mut f, &g, 1);", ["--test", "engines"], ["the_stack_ends_at_the_same_call"]), + ("the function limit", WASMQ, "src/profile.rs", ' Payload::FunctionSection(r) => count(r.count(), "functions", MAX_ITEMS)?,', " Payload::FunctionSection(_) => {}", ["--test", "review"], ["the_function_limit"]), + # The tableau: a determined outcome, word by word. + ("the sign of a determined outcome", QUANTUM, "src/quantum_tableau.rs", " let plus = (px & pz & z & !x) | (px & !pz & z & x) | (!px & pz & x & !z);", " let plus = (px & pz & z & !x) | (!px & pz & x & !z);", ["--features", "full", "--lib", "--", "quantum_tableau::tests::a_determined_measurement"], []), + ("a determined outcome's carry", QUANTUM, "src/quantum_tableau.rs", " if (x.count_ones() & 1) == 1 {\n cx = !cx;\n }", "", ["--features", "full", "--lib", "--", "quantum_tableau::tests::a_determined_measurement"], []), + # A job (§6.5). + ("the host's unsealed status, noted", QUANTUM, "src/engine/wasmq.rs", ' "status" if value == body_status(r) => {}', ' "status" => {}', ["--test", "review"], ["a_job_whose_status_or_method"]), + ("verify's read, bounded by size", CLI, "src/main.rs", " if f.metadata().map_err(|e| format!(\"{}: {e}\", p.display()))?.len() > MAX_JOB_BYTES {", " if false {", ["--test", "wasmq"], ["verify_refuses_a_file_past"]), + ("the signer is the receipt's key", WASMQ, "src/run.rs", 'signer: seal["key"].as_str().map(str::to_string)', "signer: None", ["--test", "review"], ["a_job_names_its_signer"]), + ("the signature is the engine's check of the seal", WASMQ, "src/run.rs", " signature: !c.reasons.iter().any(|x| sealing.contains(x)),", " signature: true,", ["--test", "adversarial"], ["a_receipt_changed_after_sealing"]), + ("a wasmQ job sealed in the canonical form", QUANTUM, "src/engine/wasmq.rs", ' if r["receipt"]["canon"] != "rfc8785" {', " if false {", ["--features", "full", "--test", "wasmq_verdict"], ["every_reader"]), + ("the result's members", QUANTUM, "src/engine/wasmq.rs", " members(r, \"the result\", &RESULT, &[\"fault\"], &mut out);", "", ["--test", "sweep"], ["a_job_whose_result"]), + ("the contract's form", QUANTUM, "src/engine/wasmq.rs", " contract_form(c, &mut out);", "", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the contract's method", QUANTUM, "src/engine/wasmq.rs", " if run[\"method\"] != METHOD {", " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the contract's shots", QUANTUM, "src/engine/wasmq.rs", " if !whole(&run[\"shots\"], 1, MAX_SHOTS) {", " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("what a result repeats from its contract", QUANTUM, "src/engine/wasmq.rs", " if !same(&r[name], &want) {", " if !same(&r[name], &want) && false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("fuel used within the run's budget", QUANTUM, "src/engine/wasmq.rs", " if count(&fuel[\"used\"]).is_none_or(|u| u > per_job) {", " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a call within the call's budget", QUANTUM, "src/engine/wasmq.rs", " if max.is_none_or(|m| m > per_call) {", " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a callout's fuel within its calls", QUANTUM, "src/engine/wasmq.rs", " if all.is_none_or(|a| n.zip(max).is_none_or(", " if false && all.is_none_or(|a| n.zip(max).is_none_or(", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the instances' fuel", QUANTUM, "src/engine/wasmq.rs", " if count(&fuel[\"instances\"]).is_none_or(|n| u128::from(n) > made.saturating_mul(stretches).saturating_mul(u128::from(per_instance))) {", " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a fault's fuel within its phase's budget", QUANTUM, "src/engine/wasmq.rs", " if used.is_none_or(|n| n > budget) {", " if false {", ["--features", "full", "--lib"], ["engine::wasmq::tests::a_fault_outside_a_call_is_within_the_instance_s_budget"]), + ("the fuel adds up", QUANTUM, "src/engine/wasmq.rs", " if spent.is_none() || spent != count(&fuel[\"used\"]) {", " if (spent.is_none() || spent != count(&fuel[\"used\"])) && false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("a fault after every shot", QUANTUM, "src/engine/wasmq.rs", " if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) {", " if false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("the counts' sum", QUANTUM, "src/engine/wasmq.rs", " if total.is_none() || total != shots_run {", " if (total.is_none() || total != shots_run) && false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("a record's shots are the run's", WASMQ, "src/run.rs", " if call.shot >= b.run.shots {", " if call.shot >= b.run.shots && false {", ["--test", "sweep"], ["a_record_of_a_shot"]), + ("a record's shots are in order", WASMQ, "src/run.rs", " if call.shot < last {", " if call.shot < last && false {", ["--test", "sweep"], ["a_record_of_a_shot"]), + # The command line. + ("a job read from its text", CLI, "src/main.rs", " } else if as_json || (v.get(\"request\").is_some() && v.get(\"result\").is_some()) {\n let c = job::check_text(&src);", " } else if as_json || (v.get(\"request\").is_some() && v.get(\"result\").is_some()) {\n let c = job::check(&v);", ["--test", "wasmq"], ["a_job_is_read_from_its_text_as_every_job_is"]), + ("no history for a wasmQ job", CLI, "src/main.rs", " if wasmq && hist.is_some() {", " if false {", ["--test", "wasmq"], ["json_and_lines_give"]), + # Round three: a number past 2^53 - 1 is no count; a stopped call is one of the calls + # made; a budget is at most 2^39 when packed; wasmQ's reading offers no method it cannot + # use; what is reported alone is a verdict; routing, lines and the not-judged report. + ("a whole number past 2^53 - 1 is no count", QUANTUM, "src/engine/wasmq.rs", "v.as_u64().filter(|&n| n <= MAX_EXACT).or_else(", "v.as_u64().or_else(", ["--features", "full", "--lib"], ["engine::wasmq::tests::a_number_is_read_by_its_value", "engine::wasmq::tests::a_budget_past_two_to_the_53"]), + ("a stopped call the fuel counts", QUANTUM, "src/engine/wasmq.rs", "None => out.push(format!(\"a call of {name:?} that stopped, where the fuel counts no call of it\")),", "None => {}", ["--features", "full", "--lib"], ["engine::wasmq::tests::a_call_s_fault_is_one_of_the_calls_made"]), + ("a stopped call within its callout's largest", QUANTUM, "src/engine/wasmq.rs", "Some(m) if count(&m[\"max\"]).zip(used).is_none_or(|(max, u)| u > max) =>", "Some(m) if false =>", ["--features", "full", "--lib"], ["engine::wasmq::tests::a_call_s_fault_is_one_of_the_calls_made"]), + ("a budget at most 2^39 when packed", WASMQ, "src/envelope.rs", " if v > MAX_BUDGET {", " if v > MAX_EXACT {", ["--test", "review"], ["a_budget_past_what_a_run_may_be_given"]), + ("a hybrid reading offers no other method", QUANTUM, "src/qasm/elab.rs", "if self.whole || self.callouts { String::new() }", "if self.whole { String::new() }", ["--test", "review"], ["a_refusal_for_controls"], WASMQ), + ("what is reported alone is a verdict", QUANTUM, "src/engine/job.rs", " let c = check_text(text);\n if alone(&c) {", " let c = check_text(text);\n if false && alone(&c) {", ["--features", "full", "--lib"], ["engine::job::tests::every_reason_reported_alone_is_a_verdict"]), + ("a wasmQ job goes to no other verifier", QUANTUM, "src/engine/job.rs", "!super::wasmq::is_job(&job[\"result\"]) && (job", "(job", ["--test", "verify"], ["a_wasmq_job_naming_another_schema"], CLI), + ("each line read on its own", CLI, "src/main.rs", " let c = job::check_bytes(line);", " let c = job::check_bytes(&bytes);", ["--test", "verify"], ["lines_read_each_line_on_its_own"]), + ("a file of no line refused", CLI, "src/main.rs", " if each.is_empty() {", " if false {", ["--test", "verify"], ["lines_read_each_line_on_its_own"]), + ("no envelope run for a job not judged", CLI, "src/main.rs", " if let Some(b) = &bundle\n && c.wasmq.is_some()", " if let Some(b) = &bundle", ["--test", "wasmq"], ["a_job_not_judged_is_reported_alone"]), + ("a job not judged is said to be", CLI, "src/wasmq.rs", " let Some(w) = c.wasmq.clone() else {", " let Some(w) = c.wasmq.clone().or(Some(Value::Null)) else {", ["--test", "wasmq"], ["a_job_not_judged_is_reported_alone"]), + # The one path every surface checks a job on (wai-quantum, engine::job). + ("a bundle's contract compared as it is sealed", WASMQ, "src/run.rs", "canonical(&o.contract) == canonical(c_)", "o.contract == *c_", ["--test", "review"], ["a_contract_written_otherwise"]), + ("a wasmQ job is held to its form on the one path", QUANTUM, "src/engine/job.rs", " if super::wasmq::is_job(result) {", " if false && super::wasmq::is_job(result) {", ["--features", "full", "--test", "wasmq_verdict"], ["every_reader", "a_wasmq_verdict"]), + ("a broken form is a reason", QUANTUM, "src/engine/job.rs", " c.push(WASMQ_FORM);", "", ["--features", "full", "--test", "wasmq_verdict"], ["every_reader"]), + ("the command line's verdict for a wasmQ job is the one path's", CLI, "src/main.rs", " ok &= wasmq::report(&c, &v, bundle.as_deref());", " wasmq::report(&c, &v, bundle.as_deref());", ["--test", "wasmq"], ["json_and_lines_give", "a_job_is_read_from_its_text_as_every_job_is"]), + ("a bundle that does not hold makes --json's verdict not valid", CLI, "src/wasmq.rs", " said[\"valid\"] = json!(false);", "", ["--test", "wasmq"], ["json_and_lines_give"]), +] + + +def run(name, crate, path, old, new, args, tests, tested=None): + p = os.path.join(crate, path) + s = open(p).read() + if s.count(old) != 1: + return f"MISSING {name}: the statement is in {p} {s.count(old)} times" + # Where the tests are: a crate's own library tests, or the integration tests of wasmQ + # or of the command line. + # A crate's own tests run there too when the entry asks for its features. + # An eighth member names it outright, for a file of one crate whose tests are another's. + tested = tested or (crate if "--lib" in args or "--features" in args or crate == CLI else WASMQ) + open(p, "w").write(s.replace(old, new)) + try: + cmd = ["cargo", "test", "--release"] + args + if tests: + cmd += (["--"] if "--" not in args else []) + tests + r = subprocess.run(cmd, cwd=tested, capture_output=True, text=True) + out = r.stdout + r.stderr + if "could not compile" in out: + return f"SURVIVED {name}: it did not compile: " + next((l for l in out.splitlines() if l.startswith("error")), "") + failed = [l.split()[1] for l in out.splitlines() if l.startswith("test ") and l.endswith("FAILED")] + if failed: + return f"KILLED {name}: {', '.join(failed)}" + if r.returncode != 0: + return f"KILLED {name}: the test process died ({r.returncode})" + return f"SURVIVED {name}: every test passed" + finally: + open(p, "w").write(s) + + +def main(): + words = [a for a in sys.argv[1:] if a != "--check"] + chosen = [m for m in M if not words or any(w in m[0] for w in words)] + if "--check" in sys.argv: + bad = [m[0] for m in chosen if open(os.path.join(m[1], m[2])).read().count(m[3]) != 1] + print("\n".join(f"MISSING {b}" for b in bad) or f"{len(chosen)} statements, each in the code once") + sys.exit(1 if bad else 0) + wrong = 0 + for m in chosen: + line = run(*m) + print(line, flush=True) + wrong += not line.startswith("KILLED") + print(f"{wrong} of {len(chosen)} not caught") + sys.exit(1 if wrong else 0) + + +if __name__ == "__main__": + main() diff --git a/wai/wai-wasmq/src/abi.rs b/wai/wai-wasmq/src/abi.rs new file mode 100644 index 000000000..0ea65e28a --- /dev/null +++ b/wai/wai-wasmq/src/abi.rs @@ -0,0 +1,249 @@ +//! How a callout crosses into a module (extensions/quantum-wasmq.md §5.2): an OpenQASM +//! `extern` signature lowered to a core function type, the Component Model's canonical +//! ABI for the same signature in WIT. +//! +//! A callout has one of two forms, by its widths: +//! +//! - **Scalar**, when every argument and the result have at most 64 bits: each lowers to +//! one `i64` (WIT `u64` / `s64`), bit `i` of the register bit `i` of the integer, an +//! `int[n]` sign-extended. A function of this form is a plain integer function. +//! - **List**, otherwise: each `bit[n]` argument and the result lower to a `list`, the +//! bytes `ceil(n/8)` long, bit `i` in byte `i/8` at bit `i%8`, the rest of the last byte +//! zero; `uint[k]` and `int[k]` arguments stay one `i64`. A list argument is a pointer +//! and a length into memory the module allocates with `cabi_realloc`; a list result +//! comes back as one `i32`, the address of a pointer and a length, which the host reads, +//! then calls `cabi_post_` if the module exports it. + +use crate::engine::{Instance, Trap}; +use wai_quantum::qasm::{ExternArg, ExternDecl}; +use wasmparser::ValType; + +/// How one argument or result crosses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Lower { + /// One `i64`. + Scalar, + /// A `list`. + List, +} + +/// The form of a callout: scalar when every argument and the result fit 64 bits. +pub fn form(decl: &ExternDecl) -> Lower { + if decl.args.iter().all(|a| a.bits() <= 64) && decl.ret.is_none_or(|n| n <= 64) { Lower::Scalar } else { Lower::List } +} + +/// Whether argument `a` of `decl` crosses as a list, written into the module's memory +/// after a call of its `cabi_realloc`. +pub fn arg_lowers_to_list(decl: &ExternDecl, a: ExternArg) -> bool { + arg_lower(form(decl), &a) == Lower::List +} + +/// How an argument crosses in a callout of form `f`. +fn arg_lower(f: Lower, a: &ExternArg) -> Lower { + match (f, a) { + (Lower::List, ExternArg::Bit(_)) => Lower::List, + _ => Lower::Scalar, + } +} + +/// The core function type an `extern` lowers to (§5.2). +pub fn core_type(decl: &ExternDecl) -> (Vec, Vec) { + let f = form(decl); + let mut params = Vec::new(); + for a in &decl.args { + match arg_lower(f, a) { + Lower::Scalar => params.push(ValType::I64), + Lower::List => params.extend([ValType::I32, ValType::I32]), + } + } + let results = match decl.ret { + None => vec![], + Some(_) if f == Lower::Scalar => vec![ValType::I64], + Some(_) => vec![ValType::I32], + }; + (params, results) +} + +/// Whether a call of `decl` passes a list in, so the module must export `cabi_realloc`. +pub fn takes_list(decl: &ExternDecl) -> bool { + form(decl) == Lower::List && decl.args.iter().any(|a| matches!(a, ExternArg::Bit(_))) +} + +/// Whether a call of `decl` uses the module's memory. +pub fn uses_memory(decl: &ExternDecl) -> bool { + form(decl) == Lower::List +} + +/// The bytes of `bits`, the lowest bit first, the last byte padded with zeros. +pub fn pack(bits: &[bool]) -> Vec { + let mut out = vec![0u8; bits.len().div_ceil(8)]; + for (i, &b) in bits.iter().enumerate() { + if b { + out[i / 8] |= 1 << (i % 8); + } + } + out +} + +/// A call that stopped, as a receipt names it (§6.2). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Fault { + pub reason: &'static str, + /// Words of this crate's own, the same on every host; never an engine's message. + pub detail: String, +} + +impl Fault { + fn bad(detail: impl Into) -> Fault { + Fault { reason: "bad_result", detail: detail.into() } + } +} + +/// What ended a call: the module (sealed in a fault receipt) or the host (never sealed). +#[derive(Debug)] +pub enum Stop { + Fault(Fault), + Host(String), +} + +impl From for Stop { + fn from(t: Trap) -> Stop { + match t { + Trap::Host(why) => Stop::Host(why), + t => Stop::Fault(Fault { reason: t.reason(), detail: String::new() }), + } + } +} + +/// Charges `n` of the call's fuel for bytes the host moves (§5.2). +fn charge(inst: &mut dyn Instance, n: u64) -> Result<(), Stop> { + let left = inst.fuel() - n as i64; + inst.set_fuel(left); + if left < 0 { + return Err(Trap::Fuel.into()); + } + Ok(()) +} + +fn guest(inst: &mut dyn Instance, name: &str, args: &[u64]) -> Result, Stop> { + inst.set_stack(0); + Ok(inst.call(name, args)?) +} + +/// Calls `export` as `decl` lowers it, with `input` (the arguments' bits in order) and +/// the fuel already set on `inst`: the result's bits. +pub fn call(inst: &mut dyn Instance, decl: &ExternDecl, export: &str, input: &[bool], has_post: bool) -> Result, Stop> { + let f = form(decl); + let mut args = Vec::new(); + let mut at = 0usize; + for a in &decl.args { + let n = a.bits() as usize; + let bits = &input[at..at + n]; + at += n; + match arg_lower(f, a) { + Lower::Scalar => { + let mut v = 0u64; + for (i, &b) in bits.iter().enumerate() { + v |= (b as u64) << i; + } + if let ExternArg::Int(w) = a + && *w < 64 + && v >> (w - 1) & 1 == 1 + { + v |= !0u64 << w; + } + args.push(v); + } + Lower::List => { + let bytes = pack(bits); + let len = bytes.len() as u64; + let p = guest(inst, "cabi_realloc", &[0, 0, 1, len])?; + let ptr = p.first().copied().unwrap_or(0) as u32; + charge(inst, len)?; + if !inst.write(ptr, &bytes) { + return Err(Stop::Fault(Fault::bad(format!("cabi_realloc gave {ptr}, where {len} bytes do not fit in the memory")))); + } + args.push(ptr as u64); + args.push(len); + } + } + } + let r = guest(inst, export, &args)?; + let Some(n) = decl.ret else { return Ok(Vec::new()) }; + let v = r.first().copied().unwrap_or(0); + match f { + Lower::Scalar => { + if n < 64 && v >> n != 0 { + return Err(Stop::Fault(Fault::bad(format!("{export} returned bits above the {n} its result has")))); + } + Ok((0..n).map(|i| v >> i & 1 == 1).collect()) + } + Lower::List => { + let ret = v as u32; + if !ret.is_multiple_of(4) { + return Err(Stop::Fault(Fault::bad(format!("{export} returned {ret}, which is not 4-aligned")))); + } + let pl = inst.read(ret, 8).ok_or_else(|| Stop::Fault(Fault::bad(format!("{export} returned {ret}, outside the memory"))))?; + let ptr = u32::from_le_bytes([pl[0], pl[1], pl[2], pl[3]]); + let len = u32::from_le_bytes([pl[4], pl[5], pl[6], pl[7]]); + let want = n.div_ceil(8); + if len != want { + return Err(Stop::Fault(Fault::bad(format!("{export} returned {len} bytes; its bit[{n}] takes {want}")))); + } + charge(inst, len as u64)?; + let bytes = inst.read(ptr, len).ok_or_else(|| Stop::Fault(Fault::bad(format!("{export} returned bytes outside the memory"))))?; + if n % 8 != 0 && bytes[bytes.len() - 1] >> (n % 8) != 0 { + return Err(Stop::Fault(Fault::bad(format!("{export} set bits past the {n} its result has")))); + } + if has_post { + guest(inst, &format!("cabi_post_{export}"), &[ret as u64])?; + } + Ok((0..n as usize).map(|i| bytes[i / 8] >> (i % 8) & 1 == 1).collect()) + } + } +} + +/// Passes `bytes` to the module's `wasmq-prior` export as prior `index` (§5.3). +pub fn prior(inst: &mut dyn Instance, index: u32, bytes: &[u8]) -> Result<(), Stop> { + let len = bytes.len() as u64; + let p = guest(inst, "cabi_realloc", &[0, 0, 1, len])?; + let ptr = p.first().copied().unwrap_or(0) as u32; + charge(inst, len)?; + if !inst.write(ptr, bytes) { + return Err(Stop::Fault(Fault::bad(format!("cabi_realloc gave {ptr}, where the prior's {len} bytes do not fit in the memory")))); + } + guest(inst, "wasmq-prior", &[index as u64, ptr as u64, len])?; + Ok(()) +} + +/// Calls a lifecycle export that takes nothing or a seed. +pub fn lifecycle(inst: &mut dyn Instance, name: &str, args: &[u64]) -> Result<(), Stop> { + guest(inst, name, args).map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signatures_lower_as_the_canonical_abi_does() { + let d = ExternDecl { name: "decode".into(), args: vec![ExternArg::Bit(8), ExternArg::Uint(32)], ret: Some(9) }; + assert_eq!(core_type(&d), (vec![ValType::I64, ValType::I64], vec![ValType::I64])); + let wide = ExternDecl { name: "decode".into(), args: vec![ExternArg::Bit(72)], ret: Some(144) }; + assert_eq!(core_type(&wide), (vec![ValType::I32, ValType::I32], vec![ValType::I32])); + assert!(takes_list(&wide) && !takes_list(&d)); + // One wide width makes the whole callout a list one; an integer stays an integer. + let mixed = ExternDecl { name: "decode".into(), args: vec![ExternArg::Uint(32), ExternArg::Bit(8)], ret: Some(144) }; + assert_eq!(core_type(&mixed), (vec![ValType::I64, ValType::I32, ValType::I32], vec![ValType::I32])); + let narrow_list = ExternDecl { name: "decode".into(), args: vec![ExternArg::Bit(64)], ret: Some(65) }; + assert_eq!(core_type(&narrow_list), (vec![ValType::I32, ValType::I32], vec![ValType::I32])); + let none = ExternDecl { name: "note".into(), args: vec![], ret: None }; + assert_eq!(core_type(&none), (vec![], vec![])); + } + + #[test] + fn bits_pack_lowest_first() { + assert_eq!(pack(&[true, false, false, false, false, false, false, false, false, true]), vec![0x01, 0x02]); + assert_eq!(pack(&[]), Vec::::new()); + } +} diff --git a/wai/wai-wasmq/src/bin/wasmq_vectors.rs b/wai/wai-wasmq/src/bin/wasmq_vectors.rs new file mode 100644 index 000000000..b5cf5e7fd --- /dev/null +++ b/wai/wai-wasmq/src/bin/wasmq_vectors.rs @@ -0,0 +1,43 @@ +//! Checks the wasmQ conformance vectors on this host (extensions/quantum-wasmq.md §9). +//! +//! ```sh +//! wasmq_vectors verify wai/quantum-conformance/wasmq [native,interpreter,interpreter-prepared] +//! ``` +//! +//! It prints one line a vector and a digest of all of them: the same digest on every host +//! and every engine is the claim that a run gives the same bytes everywhere. Exit 0 when +//! every vector holds, 1 otherwise. + +use wai_wasmq::run::EngineKind; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 3 || args[1] != "verify" { + eprintln!("usage: wasmq_vectors verify DIR [ENGINE,...]"); + std::process::exit(2); + } + let kinds: Vec = match args.get(3) { + Some(list) => list.split(',').map(|s| EngineKind::parse(s).unwrap_or_else(|e| { + eprintln!("{e}"); + std::process::exit(2) + })).collect(), + None => wai_wasmq::engine::available().iter().map(|s| EngineKind::parse(s).expect("a known engine")).collect(), + }; + match wai_wasmq::vectors::verify(std::path::Path::new(&args[2]), &kinds) { + Ok((lines, problems)) => { + for l in &lines { + println!("{l}"); + } + let digest = blake3::hash(lines.join("\n").as_bytes()); + println!("wasmq-vectors: {} vectors on {:?}, digest {}", lines.len(), kinds, digest.to_hex()); + for p in &problems { + eprintln!("::error::{p}"); + } + std::process::exit(if problems.is_empty() { 0 } else { 1 }); + } + Err(e) => { + eprintln!("::error::{e}"); + std::process::exit(1); + } + } +} diff --git a/wai/wai-wasmq/src/engine.rs b/wai/wai-wasmq/src/engine.rs new file mode 100644 index 000000000..ed459c2b7 --- /dev/null +++ b/wai/wai-wasmq/src/engine.rs @@ -0,0 +1,422 @@ +//! The engines a module runs on (extensions/quantum-wasmq.md §4.6), behind one interface: +//! the pinned native engine, which runs the prepared module, and the interpreter, which +//! runs the original. Both give the same results, the same fuel and the same fault. + +use crate::interp; +use crate::profile::MAX_STACK; + +/// Why a call stopped, in the profile's words (§6.2): the same on every engine. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Trap { + Unreachable, + MemoryOutOfBounds, + TableOutOfBounds, + IndirectCallNull, + IndirectCallType, + DivideByZero, + IntegerOverflow, + InvalidConversion, + /// The call's fuel ran out (§4.2). + Fuel, + /// The frames suspended at calls weigh more than the profile allows (§4.3). + Stack, + /// The host could not run the call: not the module's doing, and never sealed (§6.3). + Host(String), +} + +impl Trap { + /// The fault reason a receipt names. + pub fn reason(&self) -> &'static str { + match self { + Trap::Unreachable => "unreachable", + Trap::MemoryOutOfBounds => "memory_out_of_bounds", + Trap::TableOutOfBounds => "table_out_of_bounds", + Trap::IndirectCallNull => "indirect_call_null", + Trap::IndirectCallType => "indirect_call_type", + Trap::DivideByZero => "integer_divide_by_zero", + Trap::IntegerOverflow => "integer_overflow", + Trap::InvalidConversion => "invalid_conversion_to_integer", + Trap::Fuel => "fuel_exhausted", + Trap::Stack => "stack_exhausted", + Trap::Host(_) => "host", + } + } +} + +/// An instance of a module on some engine. +pub trait Instance { + /// Calls the export `name` with `args` (each `i32` or `i64` as 64 bits). + fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap>; + fn fuel(&mut self) -> i64; + fn set_fuel(&mut self, fuel: i64); + /// Sets the weight of the stack (§4.3): zero before each call a host makes. + fn set_stack(&mut self, weight: u32); + /// `len` bytes from `at` of the memory the module exports as `memory`, if it exports + /// one and they are inside it. + fn read(&mut self, at: u32, len: u32) -> Option>; + /// Writes `data` at `at` of the memory the module exports as `memory`, if it exports + /// one and the bytes fit inside it. + fn write(&mut self, at: u32, data: &[u8]) -> bool; +} + +/// A module compiled for an engine, from which instances are made. +pub trait Engine { + /// The engine's name, for people (never in a receipt). + fn name(&self) -> &'static str; + fn instantiate(&self) -> Result, String>; +} + +/// The engines this build has, by name. +pub fn available() -> Vec<&'static str> { + let native: &[&'static str] = if cfg!(all(feature = "wasmtime", not(target_arch = "wasm32"))) { &["native"] } else { &[] }; + [native, &["interpreter", "interpreter-prepared"]].concat() +} + +/// A trap a prepared module raised, told apart by its globals: its own fuel or stack +/// check, or the module's own `unreachable`. +pub(crate) fn refine(t: Trap, fuel: i64, stack: u32) -> Trap { + match t { + Trap::Unreachable if fuel < 0 => Trap::Fuel, + Trap::Unreachable if stack > MAX_STACK => Trap::Stack, + t => t, + } +} + +// --------------------------------------------------------------------------- +// The interpreter +// --------------------------------------------------------------------------- + +/// The interpreter, on the original module ([`interp::Metering::Own`]) or the prepared +/// one ([`interp::Metering::Module`]). +pub struct Interpreter<'a> { + module: interp::Module<'a>, +} + +impl<'a> Interpreter<'a> { + pub fn new(bytes: &'a [u8], metering: interp::Metering) -> Result, String> { + Ok(Interpreter { module: interp::Module::new(bytes, metering)? }) + } +} + +struct InterpInstance<'a> { + i: interp::Instance<'a>, + prepared: bool, +} + +impl Instance for InterpInstance<'_> { + fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap> { + self.i.call(name, args).map_err(|t| if self.prepared { refine(t, self.i.fuel(), self.i.stack_weight()) } else { t }) + } + fn fuel(&mut self) -> i64 { + self.i.fuel() + } + fn set_fuel(&mut self, fuel: i64) { + self.i.set_fuel(fuel) + } + fn set_stack(&mut self, weight: u32) { + self.i.set_stack_weight(weight) + } + fn read(&mut self, at: u32, len: u32) -> Option> { + self.i.memory()?.get(at as usize..(at as usize).checked_add(len as usize)?).map(<[u8]>::to_vec) + } + fn write(&mut self, at: u32, data: &[u8]) -> bool { + match self.i.memory_mut().and_then(|m| m.get_mut(at as usize..(at as usize).saturating_add(data.len()))) { + Some(m) => { + m.copy_from_slice(data); + true + } + None => false, + } + } +} + +impl Engine for Interpreter<'_> { + fn name(&self) -> &'static str { + "interpreter" + } + fn instantiate(&self) -> Result, String> { + let prepared = self.module_is_prepared(); + Ok(Box::new(InterpInstance { i: self.module.instantiate()?, prepared })) + } +} + +impl Interpreter<'_> { + fn module_is_prepared(&self) -> bool { + self.module.metering() == interp::Metering::Module + } +} + +// --------------------------------------------------------------------------- +// The native engine +// --------------------------------------------------------------------------- + +#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] +pub use native::{Native, native_fuel, on_its_stack}; + +#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] +mod native { + use super::*; + use crate::{meter, profile}; + use wasmtime::{Config, Engine as WEngine, Module, Store, Val, ValType}; + + /// The stack the engine gives a call's frames. The frames suspended at calls weigh at + /// most [`MAX_STACK`] (§4.3), a frame takes at most eight bytes for each unit of its + /// weight, and the last frame is one more function: under 66 MiB in all, which a test + /// holds this engine to (`tests/engines.rs`). + const WASM_STACK: usize = 128 << 20; + /// The stack of the thread a call runs on: the engine's, and room for the host's own + /// frames under it. + const THREAD_STACK: usize = WASM_STACK + (16 << 20); + + std::thread_local! { + /// Whether this thread has the stack a call needs: [`on_its_stack`]'s. + static ITS_STACK: std::cell::Cell = const { std::cell::Cell::new(false) }; + } + + /// Runs `f` on a thread with the stack a call needs, so that every call `f` makes runs + /// where it is, without a thread of its own for each: a run's calls cost the host far + /// less than a thread each would (§4.8). + pub fn on_its_stack(f: impl FnOnce() -> R + Send) -> Result { + if ITS_STACK.with(|s| s.get()) { + return Ok(f()); + } + std::thread::scope(|s| match std::thread::Builder::new().stack_size(THREAD_STACK).spawn_scoped(s, || { + ITS_STACK.with(|s| s.set(true)); + f() + }) { + Ok(h) => h.join().map_err(|_| "the run's thread panicked".to_string()), + Err(e) => Err(format!("no thread for the run: {e}")), + }) + } + + /// The engine's own fuel, a backstop under the profile's: a prepared module charges + /// itself, so this runs out only if the preparation had failed to meter something. + const BACKSTOP: u64 = 1 << 62; + + /// The pinned wasmtime release, compiling the prepared module with Cranelift, with + /// every setting the profile names (§4.6). + pub struct Native { + engine: WEngine, + module: Module, + } + + /// The engine's configuration: NaNs canonical, the relaxed vector instructions fixed + /// (and refused, with every vector instruction), no proposal past the profile's, the + /// engine's own fuel on. Threads, garbage collection, exceptions, stack switching and components + /// are not compiled into this build of the engine at all. + pub fn config(nan: bool) -> Config { + let mut c = Config::new(); + c.cranelift_nan_canonicalization(nan) + .relaxed_simd_deterministic(true) + .wasm_relaxed_simd(false) + .wasm_simd(false) + .wasm_tail_call(false) + .wasm_memory64(false) + .wasm_multi_memory(false) + .wasm_custom_page_sizes(false) + .wasm_wide_arithmetic(false) + .wasm_bulk_memory(true) + .wasm_multi_value(true) + .wasm_extended_const(true) + .consume_fuel(true) + .max_wasm_stack(WASM_STACK) + .async_stack_size(WASM_STACK + (1 << 20)); + c + } + + impl Native { + /// The engine for a prepared module ([`meter::prepare`]). + pub fn new(prepared: &[u8]) -> Result { + Native::with(prepared, true) + } + + /// [`Native::new`], NaN canonicalisation set by `nan`: only the tests that show + /// the preparation canonicalises without the engine's help use `false`. + pub fn with(prepared: &[u8], nan: bool) -> Result { + let engine = WEngine::new(&config(nan)).map_err(|e| e.to_string())?; + let module = Module::new(&engine, prepared).map_err(|e| e.to_string())?; + Ok(Native { engine, module }) + } + + /// [`Native::new`] with the engine's own stack limit at `bytes` instead of + /// [`WASM_STACK`]: only the tests that hold a frame to eight bytes for each unit + /// of its weight use it. + pub fn with_stack(prepared: &[u8], bytes: usize) -> Result { + let mut c = config(true); + c.max_wasm_stack(bytes).async_stack_size(bytes + (1 << 20)); + let engine = WEngine::new(&c).map_err(|e| e.to_string())?; + let module = Module::new(&engine, prepared).map_err(|e| e.to_string())?; + Ok(Native { engine, module }) + } + } + + struct Limits { + failed: bool, + } + + impl wasmtime::ResourceLimiter for Limits { + fn memory_growing(&mut self, _current: usize, desired: usize, maximum: Option) -> wasmtime::Result { + // Past the module's maximum the growth is refused, and the module reads -1. + Ok(maximum.is_none_or(|m| desired <= m)) + } + fn table_growing(&mut self, _current: usize, _desired: usize, _maximum: Option) -> wasmtime::Result { + // Only instantiation sizes a table: the profile refuses `table.grow`. + Ok(true) + } + fn memory_grow_failed(&mut self, error: wasmtime::Error) -> wasmtime::Result<()> { + // A growth past the module's own maximum fails in every engine and gives the + // module -1: the engine says so in these words (the pinned release; a test + // holds a growth past the maximum to -1 on every engine). Any other failure + // is a growth within the maximum that this host could not make: §6.3. + if !error.to_string().contains("exceeds memory type's limits") { + self.failed = true; + } + Ok(()) + } + } + + struct NativeInstance { + store: Store, + instance: wasmtime::Instance, + fuel: wasmtime::Global, + stack: wasmtime::Global, + memory: Option, + } + + impl Engine for Native { + fn name(&self) -> &'static str { + "native" + } + fn instantiate(&self) -> Result, String> { + let mut store = Store::new(&self.engine, Limits { failed: false }); + store.limiter(|l| l as &mut dyn wasmtime::ResourceLimiter); + store.set_fuel(BACKSTOP).map_err(|e| e.to_string())?; + let instance = wasmtime::Instance::new(&mut store, &self.module, &[]).map_err(|e| e.to_string())?; + let fuel = instance.get_global(&mut store, meter::FUEL).ok_or("the module is not prepared: no fuel global")?; + let stack = instance.get_global(&mut store, meter::STACK).ok_or("the module is not prepared: no stack global")?; + // The memory a host reaches is the one exported as `memory`, on every engine. + let memory = instance.get_memory(&mut store, profile::MEMORY); + Ok(Box::new(NativeInstance { store, instance, fuel, stack, memory })) + } + } + + impl NativeInstance { + /// The call, on this thread's stack. + fn call_here(&mut self, name: &str, args: &[u64]) -> Result, Trap> { + let f = self.instance.get_func(&mut self.store, name).ok_or_else(|| Trap::Host(format!("no export {name}")))?; + let ty = f.ty(&self.store); + let params: Vec = ty + .params() + .zip(args) + .map(|(t, &a)| match t { + ValType::I64 => Val::I64(a as i64), + ValType::F32 => Val::F32(a as u32), + ValType::F64 => Val::F64(a), + _ => Val::I32(a as u32 as i32), + }) + .collect(); + if params.len() != args.len() || ty.params().len() != args.len() { + return Err(Trap::Host(format!("{name} takes {} arguments", ty.params().len()))); + } + let mut results: Vec = ty.results().map(|_| Val::I32(0)).collect(); + self.store.set_fuel(BACKSTOP).map_err(|e| Trap::Host(e.to_string()))?; + let r = f.call(&mut self.store, ¶ms, &mut results); + if self.store.data().failed { + return Err(Trap::Host("the host could not grow the memory within the module's maximum".into())); + } + match r { + Ok(()) => Ok(results + .iter() + .map(|v| match v { + Val::I32(x) => *x as u32 as u64, + Val::I64(x) => *x as u64, + Val::F32(x) => *x as u64, + Val::F64(x) => *x, + _ => 0, + }) + .collect()), + Err(e) => { + let t = match e.downcast_ref::() { + Some(wasmtime::Trap::UnreachableCodeReached) => Trap::Unreachable, + Some(wasmtime::Trap::MemoryOutOfBounds) => Trap::MemoryOutOfBounds, + Some(wasmtime::Trap::TableOutOfBounds) => Trap::TableOutOfBounds, + Some(wasmtime::Trap::IndirectCallToNull) => Trap::IndirectCallNull, + Some(wasmtime::Trap::BadSignature) => Trap::IndirectCallType, + Some(wasmtime::Trap::IntegerOverflow) => Trap::IntegerOverflow, + Some(wasmtime::Trap::IntegerDivisionByZero) => Trap::DivideByZero, + Some(wasmtime::Trap::BadConversionToInteger) => Trap::InvalidConversion, + Some(other) => Trap::Host(format!("the engine stopped the call: {other}")), + None => Trap::Host(format!("the engine failed: {e}")), + }; + let (fuel, stack) = (self.fuel(), self.stack.get(&mut self.store).i32().unwrap_or(0) as u32); + Err(refine(t, fuel, stack)) + } + } + } + } + + impl Instance for NativeInstance { + /// The engine runs a call's frames on the stack of the thread that makes the call, + /// and stops the call only at its own limit ([`WASM_STACK`]); a thread whose stack + /// is smaller would end first, and take the process with it. So every call runs + /// on a thread that has the stack. + fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap> { + if ITS_STACK.with(|s| s.get()) { + return self.call_here(name, args); + } + std::thread::scope(|s| match std::thread::Builder::new().stack_size(THREAD_STACK).spawn_scoped(s, || self.call_here(name, args)) { + Ok(h) => h.join().unwrap_or_else(|_| Err(Trap::Host("the thread of the call panicked".into()))), + Err(e) => Err(Trap::Host(format!("no thread for the call: {e}"))), + }) + } + fn fuel(&mut self) -> i64 { + self.fuel.get(&mut self.store).i64().unwrap_or(0) + } + fn set_fuel(&mut self, fuel: i64) { + let _ = self.fuel.set(&mut self.store, Val::I64(fuel)); + } + fn set_stack(&mut self, weight: u32) { + let _ = self.stack.set(&mut self.store, Val::I32(weight as i32)); + } + fn read(&mut self, at: u32, len: u32) -> Option> { + let m = self.memory?; + m.data(&self.store).get(at as usize..(at as usize).checked_add(len as usize)?).map(<[u8]>::to_vec) + } + fn write(&mut self, at: u32, data: &[u8]) -> bool { + let Some(m) = self.memory else { return false }; + match m.data_mut(&mut self.store).get_mut(at as usize..(at as usize).saturating_add(data.len())) { + Some(d) => { + d.copy_from_slice(data); + true + } + None => false, + } + } + } + + /// The engine's own fuel for a call of `name` on the *original* module, with every + /// operator's cost the engine's default table and its variable costs one a byte and + /// one a page: the pinned engine's own count of what §4.2 counts, for the tests that + /// hold the two equal. + pub fn native_fuel(original: &[u8], calls: &[(&str, Vec)]) -> Result, u64)>, String> { + let mut c = config(true); + c.operator_cost(wasmtime::OperatorCost::new()); + let engine = WEngine::new(&c).map_err(|e| e.to_string())?; + let module = Module::new(&engine, original).map_err(|e| e.to_string())?; + let mut store = Store::new(&engine, ()); + store.set_fuel(BACKSTOP).map_err(|e| e.to_string())?; + let instance = wasmtime::Instance::new(&mut store, &module, &[]).map_err(|e| e.to_string())?; + let mut out = Vec::new(); + for (name, args) in calls { + let f = instance.get_func(&mut store, name).ok_or("no such export")?; + let ty = f.ty(&store); + let params: Vec = ty.params().zip(args).map(|(t, &a)| if matches!(t, ValType::I64) { Val::I64(a as i64) } else { Val::I32(a as i32) }).collect(); + let mut results: Vec = ty.results().map(|_| Val::I32(0)).collect(); + let before = store.get_fuel().map_err(|e| e.to_string())?; + f.call(&mut store, ¶ms, &mut results).map_err(|e| e.to_string())?; + let used = before - store.get_fuel().map_err(|e| e.to_string())?; + out.push((results.iter().map(|v| v.i64().map(|x| x as u64).or(v.i32().map(|x| x as u32 as u64)).unwrap_or(0)).collect(), used)); + } + Ok(out) + } +} diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs new file mode 100644 index 000000000..d15ad9b30 --- /dev/null +++ b/wai/wai-wasmq/src/envelope.rs @@ -0,0 +1,475 @@ +//! The envelope (extensions/quantum-wasmq.md §2): a WAI1 container whose capability is +//! `wai.quantum.wasmq`, its payload a `WQM1` bundle of the program, the modules and their +//! priors, each pinned by BLAKE3 in a canonical contract. +//! +//! ```text +//! WAI1 | u32 man_len | manifest | u32 payload_len | payload +//! payload = "WQM1" | u32 contract_len | contract (RFC 8785 JSON) | frames +//! frames = program | for each module: module, then each of its priors +//! ``` +//! +//! [`pack`] writes one; [`open`] reads one, refusing any byte it does not account for. + +use crate::abi; +use crate::profile::{self, Refusal}; +use serde_json::{Value, json}; +use wai_quantum::engine::receipt::canonical; + +/// The profile's limits that a job's form (§6.5) also states: the engine's, where every +/// verifier of a job reads them (`engine::wasmq`), so a runner and a verifier cannot differ. +pub use wai_quantum::engine::wasmq::{CALL_WORK, CODE_WORK, FUEL_WORK, MAX_BITS, MAX_BUDGET, MAX_CODE_BYTES, MAX_RESULT_BITS, MAX_ENVELOPE_BYTES, MAX_EXACT, MAX_HOST_WORK, MAX_PROGRAM_BYTES, MAX_QUBITS, MAX_SHOTS, METHOD}; + +/// The capability string. +pub const CAPABILITY: &str = "wai.quantum.wasmq"; +/// The payload's magic. +pub const MAGIC: &[u8; 4] = b"WQM1"; +/// The most steps a program takes (§4.8): a reader holds each of them for the whole run. +pub const MAX_STEPS: usize = 1 << 21; +/// The most bits the calls of a program take and give, summed over its steps (§4.8). +pub const MAX_CALLOUT_BITS: u64 = 1 << 23; +/// The most work reading a program may do (§4.8), as `wai_quantum::qasm::Bounds` counts it. +pub const MAX_READING: u64 = 1 << 26; +/// The host's part of each bit a callout takes or gives: gathered, packed, hashed and unpacked. +pub const BIT_WORK: u128 = 4; +/// A measurement or a reset, for each unit of `n` times `r` (§4.8): a random outcome +/// rewrites the tableau, and a determined one is the sign of a product of up to `n` +/// stabilizers, found a word of rows at a time; each reads the tableau about once. +pub const MEASURE_WORK: u128 = 2; +/// The bounds a program is read within (§4.8). +pub const BOUNDS: wai_quantum::qasm::Bounds = wai_quantum::qasm::Bounds { qubits: MAX_QUBITS, bits: MAX_BITS, steps: MAX_STEPS, callout_bits: MAX_CALLOUT_BITS, reading: MAX_READING }; + +pub fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +pub fn blake3_hex(b: &[u8]) -> String { + hex(blake3::hash(b).as_bytes()) +} + +/// A module and its priors, as a bundle carries them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Module { + pub name: String, + pub bytes: Vec, + /// Each prior: a name for people, and its bytes. + pub priors: Vec<(String, Vec)>, +} + +/// Which module export an `extern` calls. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Callout { + pub name: String, + pub module: usize, + pub export: String, +} + +/// How a run goes: its shots and seed, its method, whether an instance lives for a shot +/// or the whole run, and its budgets. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Run { + pub shots: u64, + pub seed: u64, + pub method: String, + /// `shot` (a fresh instance each shot) or `job` (one for the run, shots in order). + pub state: String, + pub per_call: u64, + pub per_job: u64, + pub per_init: u64, +} + +impl Run { + pub fn new(shots: u64, seed: u64) -> Run { + Run { shots, seed, method: String::new(), state: "shot".into(), per_call: 10_000_000, per_job: 1 << 34, per_init: 100_000_000 } + } +} + +/// What a bundle holds. +#[derive(Clone, Debug, PartialEq)] +pub struct Bundle { + pub program: String, + pub inputs: wai_quantum::qasm::Inputs, + pub modules: Vec, + pub callouts: Vec, + pub run: Run, +} + +/// A bundle as read from its bytes, with the bytes' digests. +#[derive(Clone, Debug)] +pub struct Opened { + pub bundle: Bundle, + pub contract: Value, + pub envelope_blake3: String, + pub bytes: Vec, +} + +fn refuse(code: &'static str, msg: impl Into) -> Refusal { + Refusal::new(code, msg) +} + +/// The program read with its callouts, and the method it runs by (§2.2, §5.1): every +/// gate one of the Clifford gates the profile names, which take no angle. +pub fn read(program: &str, inputs: &wai_quantum::qasm::Inputs) -> Result<(wai_quantum::qasm::Hybrid, String), Refusal> { + use wai_quantum::qasm::Kind::*; + if program.len() > MAX_PROGRAM_BYTES { + return Err(refuse("W016", format!("a program of {} bytes; a reader takes at most {MAX_PROGRAM_BYTES}", program.len()))); + } + let h = wai_quantum::qasm::read_callouts_within(program, inputs, BOUNDS).map_err(|d| refuse("W016", format!("{}: {}", d.code, d.message)))?; + let p = &h.reading.program; + if !h.steps.iter().any(|s| matches!(s, wai_quantum::qasm::Step::Callout { .. })) { + return Err(refuse("W016", "the program calls no `extern`: there is nothing for a module to do")); + } + if let Some(op) = p.ops.iter().find(|op| !matches!(op.kind, Id | X | Y | Z | H | S | Sdg | Sx | Sxdg | Cx | Cy | Cz | Swap)) { + return Err(refuse("W016", format!("a {:?} gate: this version of the profile runs the Clifford gates that take no angle (id, x, y, z, h, s, sdg, sx, sxdg, cx, cy, cz, swap), in whole numbers", op.kind))); + } + Ok((h, METHOD.to_string())) +} + +/// The work a run asks of its host (§4.8), in operations on a word: for each shot a fresh +/// tableau and the program's steps; for each instance the calls that make it, its module's +/// bytes and the memory it starts with; for the run its modules' bytes, compiled or +/// decoded; and the most fuel it may be given. +pub fn host_work(h: &wai_quantum::qasm::Hybrid, facts: &[profile::Facts], b: &Bundle) -> u128 { + use wai_quantum::qasm::{Instr, Step}; + let n = u128::from(h.reading.program.n_qubits); + let r = n.div_ceil(32).max(1); + let mut shot = 2 * n * r; + for s in &h.steps { + shot += match s { + Step::Instr(Instr::Gate(_)) => r, + Step::Instr(Instr::Measure { .. } | Instr::Reset(_)) => MEASURE_WORK * n * r, + Step::Callout { func, input, output } => { + let decl = &h.externs[*func as usize]; + // A list argument is a call of cabi_realloc, the export is one, and cabi_post another. + let lists = decl.args.iter().filter(|&&a| crate::abi::arg_lowers_to_list(decl, a)).count() as u128; + let post = b.callouts.get(*func as usize).and_then(|c| facts.get(c.module).map(|f| f.functions.contains_key(&format!("cabi_post_{}", c.export)))).unwrap_or(false); + CALL_WORK * (1 + lists + u128::from(post && crate::abi::uses_memory(decl))) + BIT_WORK * (input.len() + output.len()) as u128 + 1 + } + Step::Instr(_) => 1, + }; + } + let mut instance = 0; + let mut code = 0; + for (m, f) in b.modules.iter().zip(facts) { + let calls = 1 + 2 * m.priors.len() + usize::from(f.functions.contains_key("wasmq-seed")) + usize::from(f.functions.contains_key("init")); + instance += CALL_WORK * calls as u128 + m.bytes.len() as u128 + f.memory.map_or(0, |(initial, _)| u128::from(initial)) * 8192; + code += CODE_WORK * m.bytes.len() as u128; + } + let instances = if b.run.state == "shot" { b.run.shots } else { 1 }; + u128::from(b.run.shots) * shot + u128::from(instances) * instance + code + FUEL_WORK * u128::from(b.run.per_job) +} + +/// Checks a bundle as [`pack`] and [`open`] do: the program, the run, the modules, and +/// the callouts against the modules' exports. The method it fixes. +pub fn check(b: &Bundle) -> Result { + let code: usize = b.modules.iter().map(|m| m.bytes.len()).sum(); + if code > MAX_CODE_BYTES { + return Err(refuse("W010", format!("the bundle's modules hold {code} bytes among them; a bundle's code is at most {MAX_CODE_BYTES}"))); + } + let frames = b.modules.iter().flat_map(|m| std::iter::once(m.bytes.len()).chain(m.priors.iter().map(|(_, p)| p.len()))).fold(b.program.len() as u64, |n, len| n + len as u64); + if frames >= MAX_ENVELOPE_BYTES as u64 { + return Err(refuse("W013", format!("{frames} bytes of frames; an envelope holds at most {MAX_ENVELOPE_BYTES}"))); + } + let (p, method) = read(&b.program, &b.inputs)?; + let r = &b.run; + if r.shots == 0 || r.shots > MAX_SHOTS { + return Err(refuse("W017", format!("{} shots; a run takes 1 to {MAX_SHOTS}", r.shots))); + } + for (what, v) in [("seed", r.seed), ("fuel per call", r.per_call), ("fuel per run", r.per_job), ("fuel per instance", r.per_init)] { + if v > MAX_EXACT { + return Err(refuse("W017", format!("the {what}, {v}, is past 2^53 - 1: a receipt could not bind it exactly"))); + } + } + // Each budget within what a run may be given (§2.2, §4.8), as every verifier holds the + // contract to it: a writer writes nothing a verifier refuses. + for (what, v) in [("fuel per call", r.per_call), ("fuel per run", r.per_job), ("fuel per instance", r.per_init)] { + if v > MAX_BUDGET { + return Err(refuse("W017", format!("the {what}, {v}, is past 2^39, the most fuel a run may be given"))); + } + } + if let Some((name, v)) = b.inputs.iter().find(|(_, v)| v.unsigned_abs() > MAX_EXACT) { + return Err(refuse("W017", format!("the input {name}, {v}, is past 2^53 - 1 in size: the contract could not hold it exactly"))); + } + if r.state != "shot" && r.state != "job" { + return Err(refuse("W017", format!("state {:?}: an instance lives for a `shot` or the whole `job`", r.state))); + } + if !r.method.is_empty() && r.method != method { + return Err(refuse("W016", format!("the contract names method {}; this version of the profile has {method}", r.method))); + } + let mut facts = Vec::new(); + for m in &b.modules { + let named = |e: Refusal| refuse(e.code, format!("module {}: {}", m.name, e.message)); + facts.push(profile::check(&m.bytes).map_err(named)?); + // What packs, runs: the module prepares for an engine that needs it prepared. + crate::meter::prepare(&m.bytes).map_err(named)?; + } + // What a host must hold and do for a run that no fuel counts (§4.7, §4.8): whatever + // passes here runs on every host the profile is for, or none could seal the same job. + let pages: u64 = facts.iter().filter_map(|f| f.memory).map(|(_, maximum)| maximum).sum(); + if pages > profile::MAX_MEMORY_PAGES { + return Err(refuse("W003", format!("the bundle's modules hold {pages} pages among them; a run's memory is at most {}", profile::MAX_MEMORY_PAGES))); + } + let bits = u128::from(r.shots) * crate::run::width(&p.reading.program, &p.steps) as u128; + if bits > MAX_RESULT_BITS { + return Err(refuse("W017", format!("{bits} bits of results, {} shots of the program's bits; a run's results hold at most {MAX_RESULT_BITS}", r.shots))); + } + let work = host_work(&p, &facts, b); + if work > MAX_HOST_WORK { + return Err(refuse("W017", format!("{work} operations of the host's work, its own and the fuel it may be given; a run asks at most {MAX_HOST_WORK}"))); + } + // One callout for each `extern`, in the order the program declares them. + if b.callouts.len() != p.externs.len() || b.callouts.iter().zip(&p.externs).any(|(c, d)| c.name != d.name) { + return Err(refuse( + "W015", + format!("the callouts are {:?}; the program declares {:?}, and the contract names one callout for each, in that order", b.callouts.iter().map(|c| &c.name).collect::>(), p.externs.iter().map(|d| &d.name).collect::>()), + )); + } + // A module the host writes to, or reads a list from, exports what the host reaches it by. + let reachable = |f: &profile::Facts, module: &str, why: &str, realloc: bool| { + if f.memory.is_none() || !f.memory_exported { + return Err(refuse("W015", format!("{why}, and module {module} exports no memory named `{}`", profile::MEMORY))); + } + if realloc && !f.functions.contains_key("cabi_realloc") { + return Err(refuse("W015", format!("{why}, and module {module} exports no cabi_realloc(i32, i32, i32, i32) -> i32"))); + } + Ok(()) + }; + for (c, d) in b.callouts.iter().zip(&p.externs) { + let f = facts.get(c.module).ok_or_else(|| refuse("W015", format!("{} names module {}, which the bundle does not hold", d.name, c.module)))?; + let module = &b.modules[c.module].name; + let want = abi::core_type(d); + match f.functions.get(&c.export) { + None => return Err(refuse("W015", format!("module {module} exports no function {}", c.export))), + Some(got) if *got != want => { + return Err(refuse("W015", format!("{} lowers to {:?} -> {:?}; module {module} exports {} as {:?} -> {:?}", d.name, want.0, want.1, c.export, got.0, got.1))); + } + Some(_) => {} + } + if abi::uses_memory(d) { + reachable(f, module, &format!("{} passes a list", d.name), abi::takes_list(d))?; + } + } + for (k, (m, f)) in b.modules.iter().zip(&facts).enumerate() { + if !b.callouts.iter().any(|c| c.module == k) { + return Err(refuse("W015", format!("module {} is named by no callout", m.name))); + } + if !m.priors.is_empty() { + if !f.functions.contains_key("wasmq-prior") { + return Err(refuse("W015", format!("module {} has priors and exports no wasmq-prior(i32, i32, i32)", m.name))); + } + reachable(f, &m.name, "the host passes a prior", true)?; + } + } + Ok(method) +} + +/// The contract of a bundle (§2.2), with the method `check` fixed. +pub fn contract(b: &Bundle, method: &str) -> Value { + let modules: Vec = b + .modules + .iter() + .map(|m| { + json!({ + "name": m.name, + "blake3": blake3_hex(&m.bytes), + "bytes": m.bytes.len(), + "priors": m.priors.iter().map(|(n, p)| json!({ "name": n, "blake3": blake3_hex(p), "bytes": p.len() })).collect::>(), + }) + }) + .collect(); + json!({ + "profile": profile::PROFILE, + "program": { "blake3": blake3_hex(b.program.as_bytes()), "bytes": b.program.len(), "inputs": b.inputs }, + "modules": modules, + "callouts": b.callouts.iter().map(|c| json!({ "extern": c.name, "module": c.module, "export": c.export })).collect::>(), + "run": { + "shots": b.run.shots, + "seed": b.run.seed, + "method": method, + "state": b.run.state, + "fuel": { "per_call": b.run.per_call, "per_job": b.run.per_job, "per_instance": b.run.per_init }, + }, + }) +} + +/// The manifest of a bundle (§2.1): WAI 1.0, the capability with no fallback, and the +/// run's size as its informational target. +fn manifest(b: &Bundle, qubits: u32) -> Result, Refusal> { + serde_json::to_vec(&json!({ + "wai": "1.0", + "media": "binary", + "intent": "replicate", + "model_requirement": { "capability": CAPABILITY, "fallback": null }, + "conditioning": { "kind": "wasmq" }, + "target": { "qubits": qubits, "shots": b.run.shots, "seed": b.run.seed }, + })) + .map_err(|e| refuse("W013", e.to_string())) +} + +fn u32le(out: &mut Vec, v: usize) -> Result<(), Refusal> { + let v = u32::try_from(v).map_err(|_| refuse("W013", "a length past 2^32 - 1"))?; + out.extend_from_slice(&v.to_le_bytes()); + Ok(()) +} + +/// Writes the envelope of `b`, after [`check`]ing it. What it writes reads back as the +/// bundle given, or it writes nothing. +pub fn pack(b: &Bundle) -> Result, Refusal> { + let method = check(b)?; + let (h, _) = read(&b.program, &b.inputs)?; + let bytes = write_unchecked(b, &method, h.reading.program.n_qubits)?; + let mut given = b.clone(); + given.run.method = method; + if open(&bytes)?.bundle != given { + return Err(refuse("W013", "the envelope does not read back as the bundle given")); + } + Ok(bytes) +} + +/// The envelope's bytes for `b` with nothing checked: the method and the qubit count as +/// given. [`pack`] writes through this after its checks; the tests and the conformance +/// vectors use it to make envelopes a reader must refuse. +pub fn write_unchecked(b: &Bundle, method: &str, qubits: u32) -> Result, Refusal> { + let mut frames = b.program.as_bytes().to_vec(); + for m in &b.modules { + frames.extend_from_slice(&m.bytes); + for (_, prior) in &m.priors { + frames.extend_from_slice(prior); + } + } + from_parts(&manifest(b, qubits)?, MAGIC, &canonical(&contract(b, method)), &frames) +} + +/// An envelope from its parts as its bytes hold them (§2.1, §2.2): the manifest, the +/// payload's magic, the contract and the frames, with their lengths. +pub fn from_parts(manifest: &[u8], magic: &[u8; 4], contract: &[u8], frames: &[u8]) -> Result, Refusal> { + let mut payload = magic.to_vec(); + u32le(&mut payload, contract.len())?; + payload.extend_from_slice(contract); + payload.extend_from_slice(frames); + let mut out = b"WAI1".to_vec(); + u32le(&mut out, manifest.len())?; + out.extend_from_slice(manifest); + u32le(&mut out, payload.len())?; + out.extend_from_slice(&payload); + Ok(out) +} + +/// An envelope's manifest, contract and frames, as bytes. +pub type Parts = (Vec, Vec, Vec); + +/// The parts of an envelope, by its lengths alone: the manifest, the contract and the +/// frames. Nothing is checked but that the lengths fit. +pub fn parts(bytes: &[u8]) -> Result { + let mut at = 4; + let n = u32_at(bytes, &mut at)?; + let manifest = take(bytes, &mut at, n)?.to_vec(); + let n = u32_at(bytes, &mut at)?; + let payload = take(bytes, &mut at, n)?; + let mut p = 4; + let n = u32_at(payload, &mut p)?; + let contract = take(payload, &mut p, n)?.to_vec(); + Ok((manifest, contract, payload[p..].to_vec())) +} + +fn take<'a>(b: &'a [u8], at: &mut usize, n: usize) -> Result<&'a [u8], Refusal> { + let end = at.checked_add(n).filter(|&e| e <= b.len()).ok_or_else(|| refuse("W013", "the envelope ends inside a frame"))?; + let s = &b[*at..end]; + *at = end; + Ok(s) +} + +fn u32_at(b: &[u8], at: &mut usize) -> Result { + let s = take(b, at, 4)?; + Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]) as usize) +} + +fn frame<'a>(payload: &'a [u8], at: &mut usize, spec: &Value, what: &str, code: &'static str) -> Result<&'a [u8], Refusal> { + let n = spec["bytes"].as_u64().ok_or_else(|| refuse("W013", format!("{what}: no length")))? as usize; + let bytes = take(payload, at, n)?; + let want = spec["blake3"].as_str().unwrap_or(""); + let got = blake3_hex(bytes); + if got != want { + return Err(refuse(code, format!("{what}: its bytes hash to {got}, not the {want} the contract pins"))); + } + Ok(bytes) +} + +/// Reads an envelope: every byte accounted for, every frame its pinned digest, the +/// contract canonical, and the bundle [`check`]ed. +pub fn open(bytes: &[u8]) -> Result { + if bytes.len() > MAX_ENVELOPE_BYTES { + return Err(refuse("W013", format!("{} bytes; an envelope holds at most {MAX_ENVELOPE_BYTES}", bytes.len()))); + } + let mut at = 0; + if take(bytes, &mut at, 4)? != b"WAI1" { + return Err(refuse("W013", "not a WAI1 envelope")); + } + let n = u32_at(bytes, &mut at)?; + let manifest_bytes = take(bytes, &mut at, n)?; + let manifest: Value = serde_json::from_slice(manifest_bytes).map_err(|e| refuse("W013", format!("the manifest: {e}")))?; + if manifest.pointer("/model_requirement/capability").and_then(Value::as_str) != Some(CAPABILITY) { + return Err(refuse("W013", format!("the envelope's capability is not {CAPABILITY}"))); + } + let n = u32_at(bytes, &mut at)?; + let payload = take(bytes, &mut at, n)?; + if at != bytes.len() { + return Err(refuse("W013", format!("{} bytes after the payload", bytes.len() - at))); + } + let mut p = 0; + if take(payload, &mut p, 4)? != MAGIC { + return Err(refuse("W013", "the payload is not a WQM1 bundle")); + } + let n = u32_at(payload, &mut p)?; + let text = take(payload, &mut p, n)?; + let contract: Value = serde_json::from_slice(text).map_err(|e| refuse("W013", format!("the contract: {e}")))?; + if canonical(&contract) != text { + return Err(refuse("W013", "the contract is not in its canonical form (RFC 8785)")); + } + if contract["profile"] != profile::PROFILE { + return Err(refuse("W013", format!("the contract's profile is {}, not {}", contract["profile"], profile::PROFILE))); + } + let program = String::from_utf8(frame(payload, &mut p, &contract["program"], "the program", "W014")?.to_vec()).map_err(|_| refuse("W016", "the program is not UTF-8"))?; + let inputs: wai_quantum::qasm::Inputs = serde_json::from_value(contract["program"]["inputs"].clone()).map_err(|e| refuse("W013", format!("the program's inputs: {e}")))?; + let mut modules = Vec::new(); + for m in contract["modules"].as_array().ok_or_else(|| refuse("W013", "no modules"))? { + let name = m["name"].as_str().unwrap_or("").to_string(); + let module = frame(payload, &mut p, m, &format!("module {name}"), "W014")?.to_vec(); + let mut priors = Vec::new(); + for pr in m["priors"].as_array().ok_or_else(|| refuse("W013", "a module without its list of priors"))? { + let pn = pr["name"].as_str().unwrap_or("").to_string(); + priors.push((pn.clone(), frame(payload, &mut p, pr, &format!("prior {pn} of module {name}"), "W014")?.to_vec())); + } + modules.push(Module { name, bytes: module, priors }); + } + if p != payload.len() { + return Err(refuse("W013", format!("{} bytes after the frames the contract lists", payload.len() - p))); + } + let callouts = contract["callouts"] + .as_array() + .ok_or_else(|| refuse("W013", "no callouts"))? + .iter() + .map(|c| Callout { name: c["extern"].as_str().unwrap_or("").into(), module: c["module"].as_u64().unwrap_or(u64::MAX) as usize, export: c["export"].as_str().unwrap_or("").into() }) + .collect(); + let r = &contract["run"]; + let num = |v: &Value, what: &str| v.as_u64().ok_or_else(|| refuse("W017", format!("the run's {what} is not a whole number"))); + let run = Run { + shots: num(&r["shots"], "shots")?, + seed: num(&r["seed"], "seed")?, + method: r["method"].as_str().unwrap_or("").into(), + state: r["state"].as_str().unwrap_or("").into(), + per_call: num(&r["fuel"]["per_call"], "fuel per call")?, + per_job: num(&r["fuel"]["per_job"], "fuel per run")?, + per_init: num(&r["fuel"]["per_instance"], "fuel per instance")?, + }; + let bundle = Bundle { program, inputs, modules, callouts, run }; + let method = check(&bundle)?; + if canonical(&self::contract(&bundle, &method)) != text { + return Err(refuse("W013", "the contract holds members this profile does not write, or omits ones it does")); + } + // One envelope for one bundle: the manifest is the one `pack` writes for it. + let (h, _) = read(&bundle.program, &bundle.inputs)?; + if manifest_bytes != self::manifest(&bundle, h.reading.program.n_qubits)?.as_slice() { + return Err(refuse("W013", "the manifest is not the one this bundle's contract implies")); + } + Ok(Opened { bundle, contract, envelope_blake3: blake3_hex(bytes), bytes: bytes.to_vec() }) +} diff --git a/wai/wai-wasmq/src/interp.rs b/wai/wai-wasmq/src/interp.rs new file mode 100644 index 000000000..fb9636ac6 --- /dev/null +++ b/wai/wai-wasmq/src/interp.rs @@ -0,0 +1,1087 @@ +//! An interpreter for the profile (extensions/quantum-wasmq.md §4.6): the engine on hosts +//! where the pinned native engine does not run (wasm32), and a second implementation of +//! the metering everywhere. +//! +//! It runs a module as the profile reads it, not as the prepared module states it: it +//! charges each run's weight itself, counts calls itself and canonicalises each NaN +//! itself. So when it and the native engine (which runs the prepared module) agree on a +//! module's results, fuel and fault, two implementations of §4 agree. It also runs a +//! prepared module with its own metering off ([`Metering::Module`]), as a third check of +//! the preparation. +//! +//! The value stack holds every value as 64 bits: an `i32` zero-extended, a float as its +//! bits. Branch targets and stack heights are resolved when a function is compiled. + +use crate::engine::Trap; +use crate::meter::{self, ends_run, weight}; +use crate::profile::{self, MAX_STACK}; +use std::collections::BTreeMap; +use wasmparser::{BlockType, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Payload, ValType, Validator}; + +/// Who meters a run. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Metering { + /// The interpreter: the module is the original. + Own, + /// The module: a prepared module, whose globals hold the fuel and the stack's weight. + Module, +} + +#[derive(Debug)] +enum I<'a> { + Charge(u64), + Op(Operator<'a>), + Br { to: u32, keep: u32, height: u32 }, + BrIf { to: u32, keep: u32, height: u32 }, + BrTable(Box<[(u32, u32, u32)]>), + IfFalse(u32), + Goto(u32), + Return, + Call(u32), + CallIndirect(u32), + Trap, +} + +struct Func<'a> { + ty: u32, + /// The weight of its frame (§4.3). + weight: u32, + params: u32, + locals: u32, + results: u32, + code: Vec>, +} + +/// A module compiled for the interpreter. +pub struct Module<'a> { + types: Vec, + funcs: Vec>, + table: Vec>, + memory: Option<(u64, u64)>, + globals: Vec, + data: Vec<(u32, &'a [u8])>, + exports: BTreeMap, + global_exports: BTreeMap, + /// Whether the memory is exported as `memory`, where a host reaches it (§5.2). + memory_exported: bool, + metering: Metering, +} + +#[derive(Clone, Copy)] +enum Kind { + Block, + Loop, + If, + Func, +} + +struct Ctl { + kind: Kind, + height: u32, + params: u32, + results: u32, + start: u32, + /// Instructions that branch to the block's end: (instruction, entry of a table or usize::MAX). + patches: Vec<(usize, usize)>, + if_at: Option, +} + +fn arity(types: &[FuncType], bt: BlockType) -> (u32, u32) { + match bt { + BlockType::Empty => (0, 0), + BlockType::Type(_) => (0, 1), + BlockType::FuncType(i) => { + let t = &types[i as usize]; + (t.params().len() as u32, t.results().len() as u32) + } + } +} + +fn err(e: wasmparser::BinaryReaderError) -> String { + e.message().to_string() +} + +impl<'a> Module<'a> { + /// Compiles `bytes`: an original module that passes [`profile::check`] for + /// [`Metering::Own`], a prepared one for [`Metering::Module`]. + pub fn new(bytes: &'a [u8], metering: Metering) -> Result, String> { + if metering == Metering::Own { + profile::check(bytes).map_err(|r| r.to_string())?; + } + let mut v = Validator::new_with_features(profile::features()); + let mut m = Module { + types: Vec::new(), + funcs: Vec::new(), + table: Vec::new(), + memory: None, + globals: Vec::new(), + data: Vec::new(), + exports: BTreeMap::new(), + global_exports: BTreeMap::new(), + memory_exported: false, + metering, + }; + let mut func_types: Vec = Vec::new(); + let mut allocs = wasmparser::FuncValidatorAllocations::default(); + let mut consts: Vec<(ValType, Option)> = Vec::new(); + for payload in profile::parser().parse_all(bytes) { + let payload = payload.map_err(err)?; + let valid = v.payload(&payload).map_err(err)?; + match payload { + Payload::TypeSection(r) => { + for t in r.into_iter_err_on_gc_types() { + m.types.push(t.map_err(err)?); + } + } + Payload::FunctionSection(r) => { + for t in r { + func_types.push(t.map_err(err)?); + } + } + Payload::TableSection(r) => { + for t in r { + m.table = vec![None; t.map_err(err)?.ty.initial as usize]; + } + } + Payload::MemorySection(r) => { + for x in r { + let x = x.map_err(err)?; + m.memory = Some((x.initial, x.maximum.unwrap_or(x.initial))); + } + } + Payload::GlobalSection(r) => { + for g in r { + let g = g.map_err(err)?; + let val = profile::const_i64(&g.init_expr, &consts).or_else(|| float_const(&g.init_expr)).ok_or("a global whose value only running the module would say")?; + consts.push((g.ty.content_type, if g.ty.mutable { None } else { Some(val) })); + m.globals.push(match g.ty.content_type { + ValType::I32 => val as u32 as u64, + _ => val as u64, + }); + } + } + Payload::ExportSection(r) => { + for e in r { + let e = e.map_err(err)?; + match e.kind { + ExternalKind::Func => { + m.exports.insert(e.name.to_string(), e.index); + } + ExternalKind::Global => { + m.global_exports.insert(e.name.to_string(), e.index); + } + ExternalKind::Memory if e.name == profile::MEMORY => m.memory_exported = true, + _ => {} + } + } + } + Payload::ElementSection(r) => { + for el in r { + let el = el.map_err(err)?; + if let (ElementKind::Active { offset_expr, .. }, ElementItems::Functions(fs)) = (el.kind, el.items) { + let at = profile::const_i64(&offset_expr, &consts).ok_or("an element offset only running the module would say")? as u32 as usize; + for (k, f) in fs.into_iter().enumerate() { + *m.table.get_mut(at + k).ok_or("an element segment past the table")? = Some(f.map_err(err)?); + } + } + } + } + Payload::DataSection(r) => { + for d in r { + let d = d.map_err(err)?; + if let DataKind::Active { offset_expr, .. } = d.kind { + let at = profile::const_i64(&offset_expr, &consts).ok_or("a data offset only running the module would say")?; + m.data.push((at as u32, d.data)); + } + } + } + _ => {} + } + if let wasmparser::ValidPayload::Func(func, body) = valid { + let mut fv = func.into_validator(std::mem::take(&mut allocs)); + let idx = fv.index(); + let ty = func_types[idx as usize]; + let (params, results) = (m.types[ty as usize].params().len() as u32, m.types[ty as usize].results().len() as u32); + let mut locals = 0u32; + let mut lr = body.get_locals_reader().map_err(err)?; + for _ in 0..lr.get_count() { + let off = lr.original_position(); + let (n, t) = lr.read().map_err(err)?; + fv.define_locals(off, n, t).map_err(err)?; + locals += n; + } + let mut code: Vec> = Vec::new(); + let mut ctl = vec![Ctl { kind: Kind::Func, height: 0, params: 0, results, start: 0, patches: Vec::new(), if_at: None }]; + // The first run carries the function's entry (§4.2). + code.push(I::Charge(if metering == Metering::Own { meter::ENTRY } else { 0 })); + let mut run: Option = Some(0); + let mut operands = 0u32; + let mut ops = body.get_operators_reader().map_err(err)?; + while !ops.eof() { + let (op, off) = ops.read_with_offset().map_err(err)?; + let h = fv.operand_stack_height(); + if metering == Metering::Own { + let at = *run.get_or_insert_with(|| { + code.push(I::Charge(0)); + code.len() - 1 + }); + if let I::Charge(c) = &mut code[at] { + *c += weight(&op); + } + if ends_run(&op) { + run = None; + } + } + fv.op(off, &op).map_err(err)?; + operands = operands.max(fv.operand_stack_height()); + let here = |code: &Vec>| code.len() as u32; + let target = |ctl: &Vec, depth: u32| -> Result { ctl.len().checked_sub(1 + depth as usize).ok_or_else(|| "a branch past the function".to_string()) }; + match op { + Operator::Block { blockty } | Operator::Loop { blockty } | Operator::If { blockty } => { + let (p, r) = arity(&m.types, blockty); + let is_if = matches!(op, Operator::If { .. }); + let kind = match op { + Operator::Block { .. } => Kind::Block, + Operator::Loop { .. } => Kind::Loop, + _ => Kind::If, + }; + let if_at = is_if.then(|| { + code.push(I::IfFalse(u32::MAX)); + code.len() - 1 + }); + // Dead code may hold fewer operands than the block takes. + let height = h.saturating_sub(p + u32::from(is_if)); + ctl.push(Ctl { kind, height, params: p, results: r, start: here(&code), patches: Vec::new(), if_at }); + } + Operator::Else => { + let c = ctl.last_mut().ok_or("else outside if")?; + code.push(I::Goto(u32::MAX)); + c.patches.push((code.len() - 1, usize::MAX)); + if let Some(i) = c.if_at.take() { + code[i] = I::IfFalse(code.len() as u32); + } + } + Operator::End => { + let c = ctl.pop().ok_or("end outside a block")?; + let to = here(&code); + if let Some(i) = c.if_at { + code[i] = I::IfFalse(to); + } + for (p, e) in c.patches { + match &mut code[p] { + I::Br { to: t, .. } | I::BrIf { to: t, .. } | I::Goto(t) => *t = to, + I::BrTable(ts) => ts[e].0 = to, + _ => {} + } + } + if matches!(c.kind, Kind::Func) { + code.push(I::Return); + } + } + Operator::Br { relative_depth } | Operator::BrIf { relative_depth } => { + let k = target(&ctl, relative_depth)?; + let cond = matches!(op, Operator::BrIf { .. }); + if matches!(ctl[k].kind, Kind::Func) { + if cond { + // A conditional return: a branch to a return. + code.push(I::IfFalse(code.len() as u32 + 2)); + } + code.push(I::Return); + continue; + } + let (to, keep) = match ctl[k].kind { + Kind::Loop => (ctl[k].start, ctl[k].params), + _ => (u32::MAX, ctl[k].results), + }; + let height = ctl[k].height; + code.push(if cond { I::BrIf { to, keep, height } } else { I::Br { to, keep, height } }); + if to == u32::MAX { + ctl[k].patches.push((code.len() - 1, usize::MAX)); + } + } + Operator::BrTable { targets } => { + let mut list = Vec::new(); + let at = code.len(); + for (e, d) in targets.targets().chain(std::iter::once(Ok(targets.default()))).enumerate() { + let k = target(&ctl, d.map_err(err)?)?; + list.push(match ctl[k].kind { + Kind::Func => (u32::MAX - 1, ctl[k].results, 0), + Kind::Loop => (ctl[k].start, ctl[k].params, ctl[k].height), + _ => { + ctl[k].patches.push((at, e)); + (u32::MAX, ctl[k].results, ctl[k].height) + } + }); + } + code.push(I::BrTable(list.into_boxed_slice())); + } + Operator::Return => code.push(I::Return), + Operator::Call { function_index } => code.push(I::Call(function_index)), + Operator::CallIndirect { type_index, .. } => code.push(I::CallIndirect(type_index)), + Operator::Unreachable => code.push(I::Trap), + Operator::Nop => {} + other => code.push(I::Op(other)), + } + } + ops.finish().map_err(err)?; + allocs = fv.into_allocations(); + let weight = profile::frame_weight(params, results, locals, operands, body.range().end - body.range().start); + m.funcs.push(Func { ty, weight, params, locals, results, code }); + } + } + Ok(m) + } + + /// Who meters a run of this module. + pub fn metering(&self) -> Metering { + self.metering + } + + /// A fresh instance: memory zeroed and filled from the data segments, globals at + /// their initial values. + pub fn instantiate(&'a self) -> Result, String> { + let mut mem = Vec::new(); + let mut pages = 0; + if let Some((min, max)) = self.memory { + mem.try_reserve_exact((max as usize) << 16).map_err(|_| format!("this host cannot hold the module's {max} pages"))?; + mem.resize((min as usize) << 16, 0); + pages = min; + for (at, d) in &self.data { + let at = *at as usize; + mem.get_mut(at..at + d.len()).ok_or("a data segment past the memory")?.copy_from_slice(d); + } + } + Ok(Instance { m: self, stack: Vec::with_capacity(1024), mem, pages, globals: self.globals.clone(), fuel: 0, depth: 0 }) + } +} + +fn float_const(e: &wasmparser::ConstExpr<'_>) -> Option { + let mut r = e.get_operators_reader(); + match r.read().ok()? { + Operator::F32Const { value } => Some(value.bits() as i64), + Operator::F64Const { value } => Some(value.bits() as i64), + _ => None, + } +} + +/// A running instance. +pub struct Instance<'a> { + m: &'a Module<'a>, + stack: Vec, + mem: Vec, + pages: u64, + globals: Vec, + fuel: i64, + depth: u32, +} + +struct Frame { + func: u32, + pc: usize, + base: usize, +} + +const CANON32: u32 = 0x7fc0_0000; +const CANON64: u64 = 0x7ff8_0000_0000_0000; + +fn c32(x: f32) -> u64 { + if x.is_nan() { CANON32 as u64 } else { x.to_bits() as u64 } +} + +fn c64(x: f64) -> u64 { + if x.is_nan() { CANON64 } else { x.to_bits() } +} + +fn min32(a: f32, b: f32) -> f32 { + if a.is_nan() || b.is_nan() { + f32::NAN + } else if a == b { + f32::from_bits(a.to_bits() | b.to_bits()) + } else if a < b { + a + } else { + b + } +} + +fn max32(a: f32, b: f32) -> f32 { + if a.is_nan() || b.is_nan() { + f32::NAN + } else if a == b { + f32::from_bits(a.to_bits() & b.to_bits()) + } else if a > b { + a + } else { + b + } +} + +fn min64(a: f64, b: f64) -> f64 { + if a.is_nan() || b.is_nan() { + f64::NAN + } else if a == b { + f64::from_bits(a.to_bits() | b.to_bits()) + } else if a < b { + a + } else { + b + } +} + +fn max64(a: f64, b: f64) -> f64 { + if a.is_nan() || b.is_nan() { + f64::NAN + } else if a == b { + f64::from_bits(a.to_bits() & b.to_bits()) + } else if a > b { + a + } else { + b + } +} + +/// `trunc` of `x` to an integer in `[lo, hi)`, as the trapping conversions do. +fn trunc(x: f64, lo: f64, hi: f64) -> Result { + if x.is_nan() { + return Err(Trap::InvalidConversion); + } + let t = x.trunc(); + if t < lo || t >= hi { + return Err(Trap::IntegerOverflow); + } + Ok(t) +} + +impl<'a> Instance<'a> { + pub fn fuel(&self) -> i64 { + match self.m.metering { + Metering::Own => self.fuel, + Metering::Module => self.global(meter::FUEL).unwrap_or(0) as i64, + } + } + + pub fn set_fuel(&mut self, fuel: i64) { + match self.m.metering { + Metering::Own => self.fuel = fuel, + Metering::Module => self.set_global(meter::FUEL, fuel as u64), + } + } + + /// The weight of the frames suspended at a call (§4.3). + pub fn stack_weight(&self) -> u32 { + match self.m.metering { + Metering::Own => self.depth, + Metering::Module => self.global(meter::STACK).unwrap_or(0) as u32, + } + } + + pub fn set_stack_weight(&mut self, w: u32) { + match self.m.metering { + Metering::Own => self.depth = w, + Metering::Module => self.set_global(meter::STACK, w as u64), + } + } + + fn global(&self, name: &str) -> Option { + self.m.global_exports.get(name).map(|&i| self.globals[i as usize]) + } + + fn set_global(&mut self, name: &str, v: u64) { + if let Some(&i) = self.m.global_exports.get(name) { + self.globals[i as usize] = match name { + meter::STACK => v as u32 as u64, + _ => v, + }; + } + } + + /// The memory as a host reaches it: the memory the module exports as `memory`, and + /// nothing when it exports none under that name (§5.2). + pub fn memory(&self) -> Option<&[u8]> { + self.m.memory_exported.then_some(&self.mem[..]) + } + + pub fn memory_mut(&mut self) -> Option<&mut [u8]> { + self.m.memory_exported.then_some(&mut self.mem[..]) + } + + /// Calls the exported function `name` with `args` (each an `i32` or `i64` as 64 bits). + pub fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap> { + let &f = self.m.exports.get(name).ok_or_else(|| Trap::Host(format!("no export {name}")))?; + let func = &self.m.funcs[f as usize]; + if args.len() != func.params as usize { + return Err(Trap::Host(format!("{name} takes {} arguments", func.params))); + } + self.stack.clear(); + // An argument is a value of its parameter's type: a 32-bit one is its low 32 bits, + // as an engine that takes typed values reads it. + let types = self.m.types[func.ty as usize].params(); + self.stack.extend(args.iter().zip(types).map(|(&a, t)| if matches!(t, ValType::I32 | ValType::F32) { a as u32 as u64 } else { a })); + self.stack.resize(args.len() + func.locals as usize, 0); + let r = self.run(f); + if r.is_err() { + self.stack.clear(); + } + r + } + + fn charge(&mut self, n: u64) -> Result<(), Trap> { + if self.m.metering == Metering::Own { + self.fuel = self.fuel.wrapping_sub(n as i64); + if self.fuel < 0 { + return Err(Trap::Fuel); + } + } + Ok(()) + } + + fn pop(&mut self) -> u64 { + self.stack.pop().unwrap_or(0) + } + + fn unwind(&mut self, base: usize, height: u32, keep: u32) { + let to = base + height as usize; + let from = self.stack.len() - keep as usize; + if to != from { + self.stack.copy_within(from.., to); + self.stack.truncate(to + keep as usize); + } + } + + fn run(&mut self, entry: u32) -> Result, Trap> { + let m = self.m; + let mut frames = vec![Frame { func: entry, pc: 0, base: 0 }]; + loop { + let fr = frames.last_mut().ok_or(Trap::Unreachable)?; + let f = &m.funcs[fr.func as usize]; + let ops_base = fr.base + (f.params + f.locals) as usize; + // A body the profile admits ends in a return; one that does not is not run. + let Some(ins) = f.code.get(fr.pc) else { return Err(Trap::Host("a function body that does not end".into())) }; + fr.pc += 1; + match ins { + I::Charge(c) => { + if *c > 0 { + self.charge(*c)?; + } + } + I::Op(op) => { + let base = fr.base; + self.exec(op, base)?; + } + I::Br { to, keep, height } => { + self.unwind(ops_base, *height, *keep); + fr.pc = *to as usize; + } + I::BrIf { to, keep, height } => { + if self.pop() as u32 != 0 { + self.unwind(ops_base, *height, *keep); + fr.pc = *to as usize; + } + } + I::BrTable(ts) => { + let i = self.pop() as u32 as usize; + let (to, keep, height) = ts[i.min(ts.len() - 1)]; + if to == u32::MAX - 1 { + // To the function's own label: a return. + fr.pc = f.code.len() - 1; + continue; + } + self.unwind(ops_base, height, keep); + fr.pc = to as usize; + } + I::IfFalse(to) => { + if self.pop() as u32 == 0 { + fr.pc = *to as usize; + } + } + I::Goto(to) => fr.pc = *to as usize, + I::Return => { + let n = f.results as usize; + let base = fr.base; + let from = self.stack.len() - n; + self.stack.copy_within(from.., base); + self.stack.truncate(base + n); + frames.pop(); + if frames.is_empty() { + return Ok(std::mem::take(&mut self.stack)); + } + // The caller is no longer suspended at its call. + if m.metering == Metering::Own + && let Some(caller) = frames.last() + { + self.depth -= m.funcs[caller.func as usize].weight; + } + } + I::Call(g) => { + let (g, w) = (*g, f.weight); + self.suspend(w)?; + self.enter(&mut frames, g); + } + I::CallIndirect(ty) => { + // The stack is weighed before the instruction, as before a `call`: a call + // that is both past the stack and through a bad entry stops for the stack. + self.suspend(f.weight)?; + let i = self.pop() as u32 as usize; + let g = match m.table.get(i) { + None => return Err(Trap::TableOutOfBounds), + Some(None) => return Err(Trap::IndirectCallNull), + Some(Some(g)) => *g, + }; + if m.types[m.funcs[g as usize].ty as usize] != m.types[*ty as usize] { + return Err(Trap::IndirectCallType); + } + self.enter(&mut frames, g); + } + I::Trap => return Err(Trap::Unreachable), + } + } + } + + /// Adds the weight of a frame that is about to be suspended at a call (§4.3). + fn suspend(&mut self, weight: u32) -> Result<(), Trap> { + if self.m.metering == Metering::Own { + self.depth += weight; + if self.depth > MAX_STACK { + return Err(Trap::Stack); + } + } + Ok(()) + } + + fn enter(&mut self, frames: &mut Vec, g: u32) { + let callee = &self.m.funcs[g as usize]; + let base = self.stack.len() - callee.params as usize; + self.stack.resize(self.stack.len() + callee.locals as usize, 0); + frames.push(Frame { func: g, pc: 0, base }); + } + + fn addr(&mut self, offset: u64, size: usize) -> Result { + let a = self.pop() as u32 as u64 + offset; + if a + size as u64 > self.mem.len() as u64 { + return Err(Trap::MemoryOutOfBounds); + } + Ok(a as usize) + } + + fn load(&mut self, offset: u64) -> Result<[u8; N], Trap> { + let a = self.addr(offset, N)?; + let mut b = [0u8; N]; + b.copy_from_slice(&self.mem[a..a + N]); + Ok(b) + } + + fn store(&mut self, offset: u64, bytes: &[u8]) -> Result<(), Trap> { + let a = self.addr(offset, bytes.len())?; + self.mem[a..a + bytes.len()].copy_from_slice(bytes); + Ok(()) + } + + fn exec(&mut self, op: &Operator<'_>, base: usize) -> Result<(), Trap> { + use Operator::*; + let canon = self.m.metering == Metering::Own; + macro_rules! un { + ($t:ty, $f:expr) => {{ + let a = self.pop() as $t; + self.stack.push($f(a)); + }}; + } + macro_rules! bin { + ($t:ty, $f:expr) => {{ + let b = self.pop() as $t; + let a = self.pop() as $t; + self.stack.push($f(a, b)); + }}; + } + macro_rules! f32un { + ($f:expr, $canon:expr) => {{ + let a = f32::from_bits(self.pop() as u32); + let r: f32 = $f(a); + self.stack.push(if $canon { c32(r) } else { r.to_bits() as u64 }); + }}; + } + macro_rules! f64un { + ($f:expr, $canon:expr) => {{ + let a = f64::from_bits(self.pop()); + let r: f64 = $f(a); + self.stack.push(if $canon { c64(r) } else { r.to_bits() }); + }}; + } + macro_rules! f32bin { + ($f:expr) => {{ + let b = f32::from_bits(self.pop() as u32); + let a = f32::from_bits(self.pop() as u32); + let r: f32 = $f(a, b); + self.stack.push(if canon { c32(r) } else { r.to_bits() as u64 }); + }}; + } + macro_rules! f64bin { + ($f:expr) => {{ + let b = f64::from_bits(self.pop()); + let a = f64::from_bits(self.pop()); + let r: f64 = $f(a, b); + self.stack.push(if canon { c64(r) } else { r.to_bits() }); + }}; + } + macro_rules! f32cmp { + ($f:expr) => {{ + let b = f32::from_bits(self.pop() as u32); + let a = f32::from_bits(self.pop() as u32); + self.stack.push(u64::from($f(a, b))); + }}; + } + macro_rules! f64cmp { + ($f:expr) => {{ + let b = f64::from_bits(self.pop()); + let a = f64::from_bits(self.pop()); + self.stack.push(u64::from($f(a, b))); + }}; + } + match *op { + Drop => { + self.pop(); + } + Select | TypedSelect { .. } => { + let c = self.pop() as u32; + let b = self.pop(); + let a = self.pop(); + self.stack.push(if c != 0 { a } else { b }); + } + LocalGet { local_index } => { + let v = self.stack[base + local_index as usize]; + self.stack.push(v); + } + LocalSet { local_index } => { + let v = self.pop(); + self.stack[base + local_index as usize] = v; + } + LocalTee { local_index } => { + let v = *self.stack.last().unwrap_or(&0); + self.stack[base + local_index as usize] = v; + } + GlobalGet { global_index } => self.stack.push(self.globals[global_index as usize]), + GlobalSet { global_index } => { + let v = self.pop(); + self.globals[global_index as usize] = v; + } + I32Load { memarg } => { + let b = self.load::<4>(memarg.offset)?; + self.stack.push(u32::from_le_bytes(b) as u64); + } + I64Load { memarg } | F64Load { memarg } => { + let b = self.load::<8>(memarg.offset)?; + self.stack.push(u64::from_le_bytes(b)); + } + F32Load { memarg } => { + let b = self.load::<4>(memarg.offset)?; + self.stack.push(u32::from_le_bytes(b) as u64); + } + I32Load8S { memarg } => { + let b = self.load::<1>(memarg.offset)?; + self.stack.push(b[0] as i8 as i32 as u32 as u64); + } + I32Load8U { memarg } => { + let b = self.load::<1>(memarg.offset)?; + self.stack.push(b[0] as u64); + } + I32Load16S { memarg } => { + let b = self.load::<2>(memarg.offset)?; + self.stack.push(i16::from_le_bytes(b) as i32 as u32 as u64); + } + I32Load16U { memarg } => { + let b = self.load::<2>(memarg.offset)?; + self.stack.push(u16::from_le_bytes(b) as u64); + } + I64Load8S { memarg } => { + let b = self.load::<1>(memarg.offset)?; + self.stack.push(b[0] as i8 as i64 as u64); + } + I64Load8U { memarg } => { + let b = self.load::<1>(memarg.offset)?; + self.stack.push(b[0] as u64); + } + I64Load16S { memarg } => { + let b = self.load::<2>(memarg.offset)?; + self.stack.push(i16::from_le_bytes(b) as i64 as u64); + } + I64Load16U { memarg } => { + let b = self.load::<2>(memarg.offset)?; + self.stack.push(u16::from_le_bytes(b) as u64); + } + I64Load32S { memarg } => { + let b = self.load::<4>(memarg.offset)?; + self.stack.push(i32::from_le_bytes(b) as i64 as u64); + } + I64Load32U { memarg } => { + let b = self.load::<4>(memarg.offset)?; + self.stack.push(u32::from_le_bytes(b) as u64); + } + I32Store { memarg } | F32Store { memarg } => { + let v = self.pop() as u32; + self.store(memarg.offset, &v.to_le_bytes())?; + } + I64Store { memarg } | F64Store { memarg } => { + let v = self.pop(); + self.store(memarg.offset, &v.to_le_bytes())?; + } + I32Store8 { memarg } | I64Store8 { memarg } => { + let v = self.pop() as u8; + self.store(memarg.offset, &[v])?; + } + I32Store16 { memarg } | I64Store16 { memarg } => { + let v = self.pop() as u16; + self.store(memarg.offset, &v.to_le_bytes())?; + } + I64Store32 { memarg } => { + let v = self.pop() as u32; + self.store(memarg.offset, &v.to_le_bytes())?; + } + MemorySize { .. } => self.stack.push(self.pages), + MemoryGrow { .. } => { + let delta = self.pop() as u32 as u64; + self.charge(delta * meter::PAGE_FUEL)?; + let max = self.m.memory.map(|(_, max)| max).unwrap_or(0); + if self.pages + delta > max { + self.stack.push(u32::MAX as u64); + } else { + let old = self.pages; + self.pages += delta; + self.mem.resize((self.pages as usize) << 16, 0); + self.stack.push(old); + } + } + MemoryCopy { .. } => { + let n = self.pop() as u32 as u64; + let s = self.pop() as u32 as u64; + let d = self.pop() as u32 as u64; + self.charge(n)?; + let len = self.mem.len() as u64; + if s + n > len || d + n > len { + return Err(Trap::MemoryOutOfBounds); + } + self.mem.copy_within(s as usize..(s + n) as usize, d as usize); + } + MemoryFill { .. } => { + let n = self.pop() as u32 as u64; + let v = self.pop() as u8; + let d = self.pop() as u32 as u64; + self.charge(n)?; + if d + n > self.mem.len() as u64 { + return Err(Trap::MemoryOutOfBounds); + } + self.mem[d as usize..(d + n) as usize].fill(v); + } + I32Const { value } => self.stack.push(value as u32 as u64), + I64Const { value } => self.stack.push(value as u64), + F32Const { value } => self.stack.push(value.bits() as u64), + F64Const { value } => self.stack.push(value.bits()), + I32Eqz => un!(u32, |a: u32| u64::from(a == 0)), + I32Eq => bin!(u32, |a: u32, b: u32| u64::from(a == b)), + I32Ne => bin!(u32, |a: u32, b: u32| u64::from(a != b)), + I32LtS => bin!(u32, |a: u32, b: u32| u64::from((a as i32) < (b as i32))), + I32LtU => bin!(u32, |a: u32, b: u32| u64::from(a < b)), + I32GtS => bin!(u32, |a: u32, b: u32| u64::from((a as i32) > (b as i32))), + I32GtU => bin!(u32, |a: u32, b: u32| u64::from(a > b)), + I32LeS => bin!(u32, |a: u32, b: u32| u64::from((a as i32) <= (b as i32))), + I32LeU => bin!(u32, |a: u32, b: u32| u64::from(a <= b)), + I32GeS => bin!(u32, |a: u32, b: u32| u64::from((a as i32) >= (b as i32))), + I32GeU => bin!(u32, |a: u32, b: u32| u64::from(a >= b)), + I64Eqz => un!(u64, |a: u64| u64::from(a == 0)), + I64Eq => bin!(u64, |a: u64, b: u64| u64::from(a == b)), + I64Ne => bin!(u64, |a: u64, b: u64| u64::from(a != b)), + I64LtS => bin!(u64, |a: u64, b: u64| u64::from((a as i64) < (b as i64))), + I64LtU => bin!(u64, |a: u64, b: u64| u64::from(a < b)), + I64GtS => bin!(u64, |a: u64, b: u64| u64::from((a as i64) > (b as i64))), + I64GtU => bin!(u64, |a: u64, b: u64| u64::from(a > b)), + I64LeS => bin!(u64, |a: u64, b: u64| u64::from((a as i64) <= (b as i64))), + I64LeU => bin!(u64, |a: u64, b: u64| u64::from(a <= b)), + I64GeS => bin!(u64, |a: u64, b: u64| u64::from((a as i64) >= (b as i64))), + I64GeU => bin!(u64, |a: u64, b: u64| u64::from(a >= b)), + F32Eq => f32cmp!(|a: f32, b: f32| a == b), + F32Ne => f32cmp!(|a: f32, b: f32| a != b), + F32Lt => f32cmp!(|a: f32, b: f32| a < b), + F32Gt => f32cmp!(|a: f32, b: f32| a > b), + F32Le => f32cmp!(|a: f32, b: f32| a <= b), + F32Ge => f32cmp!(|a: f32, b: f32| a >= b), + F64Eq => f64cmp!(|a: f64, b: f64| a == b), + F64Ne => f64cmp!(|a: f64, b: f64| a != b), + F64Lt => f64cmp!(|a: f64, b: f64| a < b), + F64Gt => f64cmp!(|a: f64, b: f64| a > b), + F64Le => f64cmp!(|a: f64, b: f64| a <= b), + F64Ge => f64cmp!(|a: f64, b: f64| a >= b), + I32Clz => un!(u32, |a: u32| a.leading_zeros() as u64), + I32Ctz => un!(u32, |a: u32| a.trailing_zeros() as u64), + I32Popcnt => un!(u32, |a: u32| a.count_ones() as u64), + I32Add => bin!(u32, |a: u32, b: u32| a.wrapping_add(b) as u64), + I32Sub => bin!(u32, |a: u32, b: u32| a.wrapping_sub(b) as u64), + I32Mul => bin!(u32, |a: u32, b: u32| a.wrapping_mul(b) as u64), + I32DivS => { + let b = self.pop() as u32 as i32; + let a = self.pop() as u32 as i32; + if b == 0 { + return Err(Trap::DivideByZero); + } + if a == i32::MIN && b == -1 { + return Err(Trap::IntegerOverflow); + } + self.stack.push((a / b) as u32 as u64); + } + I32DivU => { + let b = self.pop() as u32; + let a = self.pop() as u32; + self.stack.push(a.checked_div(b).ok_or(Trap::DivideByZero)? as u64); + } + I32RemS => { + let b = self.pop() as u32 as i32; + let a = self.pop() as u32 as i32; + if b == 0 { + return Err(Trap::DivideByZero); + } + self.stack.push(a.wrapping_rem(b) as u32 as u64); + } + I32RemU => { + let b = self.pop() as u32; + let a = self.pop() as u32; + self.stack.push(a.checked_rem(b).ok_or(Trap::DivideByZero)? as u64); + } + I32And => bin!(u32, |a: u32, b: u32| (a & b) as u64), + I32Or => bin!(u32, |a: u32, b: u32| (a | b) as u64), + I32Xor => bin!(u32, |a: u32, b: u32| (a ^ b) as u64), + I32Shl => bin!(u32, |a: u32, b: u32| a.wrapping_shl(b) as u64), + I32ShrS => bin!(u32, |a: u32, b: u32| (a as i32).wrapping_shr(b) as u32 as u64), + I32ShrU => bin!(u32, |a: u32, b: u32| a.wrapping_shr(b) as u64), + I32Rotl => bin!(u32, |a: u32, b: u32| a.rotate_left(b % 32) as u64), + I32Rotr => bin!(u32, |a: u32, b: u32| a.rotate_right(b % 32) as u64), + I64Clz => un!(u64, |a: u64| a.leading_zeros() as u64), + I64Ctz => un!(u64, |a: u64| a.trailing_zeros() as u64), + I64Popcnt => un!(u64, |a: u64| a.count_ones() as u64), + I64Add => bin!(u64, |a: u64, b: u64| a.wrapping_add(b)), + I64Sub => bin!(u64, |a: u64, b: u64| a.wrapping_sub(b)), + I64Mul => bin!(u64, |a: u64, b: u64| a.wrapping_mul(b)), + I64DivS => { + let b = self.pop() as i64; + let a = self.pop() as i64; + if b == 0 { + return Err(Trap::DivideByZero); + } + if a == i64::MIN && b == -1 { + return Err(Trap::IntegerOverflow); + } + self.stack.push((a / b) as u64); + } + I64DivU => { + let b = self.pop(); + let a = self.pop(); + self.stack.push(a.checked_div(b).ok_or(Trap::DivideByZero)?); + } + I64RemS => { + let b = self.pop() as i64; + let a = self.pop() as i64; + if b == 0 { + return Err(Trap::DivideByZero); + } + self.stack.push(a.wrapping_rem(b) as u64); + } + I64RemU => { + let b = self.pop(); + let a = self.pop(); + self.stack.push(a.checked_rem(b).ok_or(Trap::DivideByZero)?); + } + I64And => bin!(u64, |a: u64, b: u64| a & b), + I64Or => bin!(u64, |a: u64, b: u64| a | b), + I64Xor => bin!(u64, |a: u64, b: u64| a ^ b), + I64Shl => bin!(u64, |a: u64, b: u64| a.wrapping_shl(b as u32)), + I64ShrS => bin!(u64, |a: u64, b: u64| (a as i64).wrapping_shr(b as u32) as u64), + I64ShrU => bin!(u64, |a: u64, b: u64| a.wrapping_shr(b as u32)), + I64Rotl => bin!(u64, |a: u64, b: u64| a.rotate_left((b % 64) as u32)), + I64Rotr => bin!(u64, |a: u64, b: u64| a.rotate_right((b % 64) as u32)), + F32Abs => f32un!(|a: f32| a.abs(), false), + F32Neg => f32un!(|a: f32| -a, false), + F32Ceil => f32un!(|a: f32| a.ceil(), canon), + F32Floor => f32un!(|a: f32| a.floor(), canon), + F32Trunc => f32un!(|a: f32| a.trunc(), canon), + F32Nearest => f32un!(|a: f32| a.round_ties_even(), canon), + F32Sqrt => f32un!(|a: f32| a.sqrt(), canon), + F32Add => f32bin!(|a: f32, b: f32| a + b), + F32Sub => f32bin!(|a: f32, b: f32| a - b), + F32Mul => f32bin!(|a: f32, b: f32| a * b), + F32Div => f32bin!(|a: f32, b: f32| a / b), + F32Min => f32bin!(min32), + F32Max => f32bin!(max32), + F32Copysign => { + let b = f32::from_bits(self.pop() as u32); + let a = f32::from_bits(self.pop() as u32); + self.stack.push(a.copysign(b).to_bits() as u64); + } + F64Abs => f64un!(|a: f64| a.abs(), false), + F64Neg => f64un!(|a: f64| -a, false), + F64Ceil => f64un!(|a: f64| a.ceil(), canon), + F64Floor => f64un!(|a: f64| a.floor(), canon), + F64Trunc => f64un!(|a: f64| a.trunc(), canon), + F64Nearest => f64un!(|a: f64| a.round_ties_even(), canon), + F64Sqrt => f64un!(|a: f64| a.sqrt(), canon), + F64Add => f64bin!(|a: f64, b: f64| a + b), + F64Sub => f64bin!(|a: f64, b: f64| a - b), + F64Mul => f64bin!(|a: f64, b: f64| a * b), + F64Div => f64bin!(|a: f64, b: f64| a / b), + F64Min => f64bin!(min64), + F64Max => f64bin!(max64), + F64Copysign => { + let b = f64::from_bits(self.pop()); + let a = f64::from_bits(self.pop()); + self.stack.push(a.copysign(b).to_bits()); + } + I32WrapI64 => un!(u64, |a: u64| a as u32 as u64), + I32TruncF32S | I32TruncF64S | I32TruncF32U | I32TruncF64U | I64TruncF32S | I64TruncF64S | I64TruncF32U | I64TruncF64U => { + let raw = self.pop(); + let x = match op { + I32TruncF32S | I32TruncF32U | I64TruncF32S | I64TruncF32U => f32::from_bits(raw as u32) as f64, + _ => f64::from_bits(raw), + }; + let v = match op { + I32TruncF32S | I32TruncF64S => trunc(x, -2147483648.0, 2147483648.0)? as i32 as u32 as u64, + I32TruncF32U | I32TruncF64U => trunc(x, -0.0, 4294967296.0).or_else(|e| if x > -1.0 && x < 0.0 { Ok(0.0) } else { Err(e) })? as u32 as u64, + I64TruncF32S | I64TruncF64S => trunc(x, -9223372036854775808.0, 9223372036854775808.0)? as i64 as u64, + _ => trunc(x, -0.0, 18446744073709551616.0).or_else(|e| if x > -1.0 && x < 0.0 { Ok(0.0) } else { Err(e) })? as u64, + }; + self.stack.push(v); + } + I32TruncSatF32S => un!(u64, |a: u64| f32::from_bits(a as u32) as i32 as u32 as u64), + I32TruncSatF32U => un!(u64, |a: u64| f32::from_bits(a as u32) as u32 as u64), + I32TruncSatF64S => un!(u64, |a: u64| f64::from_bits(a) as i32 as u32 as u64), + I32TruncSatF64U => un!(u64, |a: u64| f64::from_bits(a) as u32 as u64), + I64TruncSatF32S => un!(u64, |a: u64| f32::from_bits(a as u32) as i64 as u64), + I64TruncSatF32U => un!(u64, |a: u64| f32::from_bits(a as u32) as u64), + I64TruncSatF64S => un!(u64, |a: u64| f64::from_bits(a) as i64 as u64), + I64TruncSatF64U => un!(u64, |a: u64| f64::from_bits(a) as u64), + I64ExtendI32S => un!(u64, |a: u64| a as u32 as i32 as i64 as u64), + I64ExtendI32U => un!(u64, |a: u64| a as u32 as u64), + F32ConvertI32S => un!(u64, |a: u64| (a as u32 as i32 as f32).to_bits() as u64), + F32ConvertI32U => un!(u64, |a: u64| (a as u32 as f32).to_bits() as u64), + F32ConvertI64S => un!(u64, |a: u64| (a as i64 as f32).to_bits() as u64), + F32ConvertI64U => un!(u64, |a: u64| (a as f32).to_bits() as u64), + F32DemoteF64 => { + let a = f64::from_bits(self.pop()); + let r = a as f32; + self.stack.push(if canon { c32(r) } else { r.to_bits() as u64 }); + } + F64ConvertI32S => un!(u64, |a: u64| (a as u32 as i32 as f64).to_bits()), + F64ConvertI32U => un!(u64, |a: u64| (a as u32 as f64).to_bits()), + F64ConvertI64S => un!(u64, |a: u64| (a as i64 as f64).to_bits()), + F64ConvertI64U => un!(u64, |a: u64| (a as f64).to_bits()), + F64PromoteF32 => { + let a = f32::from_bits(self.pop() as u32); + let r = a as f64; + self.stack.push(if canon { c64(r) } else { r.to_bits() }); + } + I32ReinterpretF32 | F32ReinterpretI32 => {} + I64ReinterpretF64 | F64ReinterpretI64 => {} + I32Extend8S => un!(u64, |a: u64| a as u8 as i8 as i32 as u32 as u64), + I32Extend16S => un!(u64, |a: u64| a as u16 as i16 as i32 as u32 as u64), + I64Extend8S => un!(u64, |a: u64| a as u8 as i8 as i64 as u64), + I64Extend16S => un!(u64, |a: u64| a as u16 as i16 as i64 as u64), + I64Extend32S => un!(u64, |a: u64| a as u32 as i32 as i64 as u64), + ref other => return Err(Trap::Host(format!("an instruction the profile does not take: {other:?}"))), + } + Ok(()) + } +} diff --git a/wai/wai-wasmq/src/lib.rs b/wai/wai-wasmq/src/lib.rs new file mode 100644 index 000000000..734fe99d4 --- /dev/null +++ b/wai/wai-wasmq/src/lib.rs @@ -0,0 +1,29 @@ +//! wasmQ: a deterministic, metered, receipted WebAssembly profile for hybrid +//! quantum-classical programs (`wai/extensions/quantum-wasmq.md`). +//! +//! An OpenQASM 3 program declares the classical functions it calls in the middle of a +//! circuit with `extern`. A wasmQ bundle carries that program and the WebAssembly modules +//! that define those functions, in one content-addressed WAI envelope ([`envelope`]). A +//! module is held to a profile before anything runs it ([`profile`]): Lime1, nothing +//! imported, memory, size and stack bounded. Fuel, the stack's weight and the NaN a float +//! instruction makes are defined on the module as written, so every runner gives a call +//! the same results, the same fuel and the same fault. Two engines run a module here +//! ([`engine`]): the pinned native engine, on the module prepared to hold those rules +//! itself ([`meter`]), and an interpreter of this crate's own ([`interp`]), the only one +//! on wasm32. A run ([`run`]) goes shot by shot over a stabilizer tableau, in whole +//! numbers throughout, each callout a metered call ([`abi`]), and seals the result in the +//! job receipt `wai-quantum verify` checks: the fuel used, the modules' digests, a digest +//! of every call, and the fault that stopped it, if one did. + +pub mod abi; +pub mod engine; +pub mod envelope; +pub mod interp; +pub mod meter; +pub mod profile; +pub mod reference; +pub mod run; +pub mod vectors; + +pub use envelope::{Bundle, Callout, Module, Opened, Run, open, pack}; +pub use run::{EngineKind, Error, run, verify}; diff --git a/wai/wai-wasmq/src/meter.rs b/wai/wai-wasmq/src/meter.rs new file mode 100644 index 000000000..79e873fe3 --- /dev/null +++ b/wai/wai-wasmq/src/meter.rs @@ -0,0 +1,489 @@ +//! The prepared module (extensions/quantum-wasmq.md §4.4): the module rewritten so that it +//! meters its own fuel, weighs its own stack and returns only canonical NaNs, whatever +//! engine runs it. +//! +//! [`prepare`] is a pure function of the module's bytes. It copies every instruction of +//! the module byte for byte and adds, and only adds: +//! +//! - at the start of each *run* (§4.2), a charge of the run's weight against the global +//! `__wasmq_fuel`, trapping when the fuel falls below zero; +//! - before `memory.copy` and `memory.fill`, a charge of the byte count, and before +//! `memory.grow`, a charge of [`PAGE_FUEL`] for each page; +//! - around each `call` and `call_indirect`, the calling function's frame weight (§4.3) +//! added to the global `__wasmq_stack` and taken off again, trapping past +//! [`MAX_STACK`]; +//! - after each instruction that may make a NaN, a call that makes it the canonical NaN. +//! +//! Custom sections are dropped. The two globals are exported, so a host reads the fuel +//! left and the stack's weight after a call, and tells a fuel trap (`__wasmq_fuel < 0`) +//! and a stack trap (`__wasmq_stack > MAX_STACK`) from the module's own. +//! +//! The prepared module is this implementation's way to hold §4.2, §4.3 and §4.5 on an +//! engine that does not hold them itself. Its bytes are not part of the profile, and no +//! receipt names them. + +use crate::profile::{self, MAX_STACK, Refusal}; +use wasmparser::{Operator, Payload}; + +/// The exported fuel global. +pub const FUEL: &str = "__wasmq_fuel"; +/// The exported stack-weight global. +pub const STACK: &str = "__wasmq_stack"; + +/// The weight of an instruction (§4.2): the default operator cost table of the pinned +/// engine, stated in full so another engine implements it. Eight instructions that +/// generate no work weigh nothing; every other weighs one. Entering a function weighs +/// one more ([`ENTRY`]). +pub fn weight(op: &Operator<'_>) -> u64 { + use Operator::*; + match op { + Nop | Drop | Block { .. } | Loop { .. } | Unreachable | Return | Else | End => 0, + _ => 1, + } +} + +/// The weight of entering a function (§4.2), charged with the first run of its body. +pub const ENTRY: u64 = 1; + +/// Whether a run ends after `op` (§4.2): it transfers control, or is the target of one. +pub fn ends_run(op: &Operator<'_>) -> bool { + use Operator::*; + matches!(op, Block { .. } | Loop { .. } | If { .. } | Else | End | Br { .. } | BrIf { .. } | BrTable { .. } | Return | Call { .. } | CallIndirect { .. } | Unreachable) +} + +/// What `memory.grow` is charged for each page it is asked for (§4.2): as much as zeroing +/// the page's 64 KiB costs a host, whether or not the memory grows. +pub const PAGE_FUEL: u64 = 1024; + +/// Whether the instruction has a variable charge (§4.2): `memory.copy` and `memory.fill` +/// are charged one for each byte, `memory.grow` [`PAGE_FUEL`] for each page. +pub fn variable(op: &Operator<'_>) -> bool { + use Operator::*; + matches!(op, MemoryCopy { .. } | MemoryFill { .. } | MemoryGrow { .. }) +} + +/// Whether the instruction's result is canonicalised (§4.5): an arithmetic instruction +/// on floating-point values, whose NaN a host would otherwise choose. +pub fn nan_result(op: &Operator<'_>) -> Option { + use Operator::*; + match op { + F32Add | F32Sub | F32Mul | F32Div | F32Min | F32Max | F32Sqrt | F32Ceil | F32Floor | F32Trunc | F32Nearest | F32DemoteF64 => Some(false), + F64Add | F64Sub | F64Mul | F64Div | F64Min | F64Max | F64Sqrt | F64Ceil | F64Floor | F64Trunc | F64Nearest | F64PromoteF32 => Some(true), + _ => None, + } +} + +/// What may be left out of a preparation, for the tests that show each part is needed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Parts { + pub fuel: bool, + pub stack: bool, + pub nan: bool, +} + +impl Parts { + pub const ALL: Parts = Parts { fuel: true, stack: true, nan: true }; +} + +pub(crate) fn leb_u(out: &mut Vec, mut v: u64) { + loop { + let b = (v & 0x7f) as u8; + v >>= 7; + if v == 0 { + out.push(b); + return; + } + out.push(b | 0x80); + } +} + +pub(crate) fn leb_s(out: &mut Vec, mut v: i64) { + loop { + let b = (v & 0x7f) as u8; + v >>= 7; + if (v == 0 && b & 0x40 == 0) || (v == -1 && b & 0x40 != 0) { + out.push(b); + return; + } + out.push(b | 0x80); + } +} + +fn read_leb(bytes: &[u8]) -> (u64, usize) { + let (mut v, mut shift, mut i) = (0u64, 0, 0); + loop { + let b = bytes[i]; + v |= ((b & 0x7f) as u64) << shift; + i += 1; + if b & 0x80 == 0 { + return (v, i); + } + shift += 7; + } +} + +fn section(out: &mut Vec, id: u8, body: &[u8]) { + out.push(id); + leb_u(out, body.len() as u64); + out.extend_from_slice(body); +} + +/// A vector section's body with `extra` entries appended (`more` is their encoding). +fn appended(raw: Option<&[u8]>, extra: u64, more: &[u8]) -> Vec { + let (n, at) = raw.map(read_leb).unwrap_or((0, 0)); + let mut b = Vec::new(); + leb_u(&mut b, n + extra); + if let Some(r) = raw { + b.extend_from_slice(&r[at..]); + } + b.extend_from_slice(more); + b +} + +struct Globals { + fuel: u32, + stack: u32, +} + +fn charge_const(out: &mut Vec, g: &Globals, cost: u64) { + out.push(0x23); + leb_u(out, g.fuel as u64); + out.push(0x42); + leb_s(out, cost as i64); + out.push(0x7d); + fuel_check(out, g); +} + +/// `global.set F; global.get F; i64.const 0; i64.lt_s; if unreachable end` (WebAssembly +/// has no `global.tee`). +fn fuel_check(out: &mut Vec, g: &Globals) { + out.push(0x24); + leb_u(out, g.fuel as u64); + out.push(0x23); + leb_u(out, g.fuel as u64); + out.extend_from_slice(&[0x42, 0x00, 0x53, 0x04, 0x40, 0x00, 0x0b]); +} + +/// The charge of the i32 on top of the stack times `scale`, the i32 kept there (in local `t`). +fn charge_top(out: &mut Vec, g: &Globals, t: u32, scale: u64) { + out.push(0x22); + leb_u(out, t as u64); + out.push(0x23); + leb_u(out, g.fuel as u64); + out.push(0x20); + leb_u(out, t as u64); + out.push(0xad); + if scale != 1 { + out.push(0x42); + leb_s(out, scale as i64); + out.push(0x7e); + } + out.push(0x7d); + fuel_check(out, g); +} + +/// Before a call: `global.get S; i32.const w; i32.add; global.set S; global.get S; +/// i32.const MAX_STACK; i32.gt_u; if unreachable end`, `w` the calling function's weight. +fn stack_in(out: &mut Vec, g: &Globals, w: u32) { + out.push(0x23); + leb_u(out, g.stack as u64); + out.push(0x41); + leb_s(out, w as i64); + out.extend_from_slice(&[0x6a, 0x24]); + leb_u(out, g.stack as u64); + out.push(0x23); + leb_u(out, g.stack as u64); + out.push(0x41); + leb_s(out, MAX_STACK as i64); + out.extend_from_slice(&[0x4b, 0x04, 0x40, 0x00, 0x0b]); +} + +/// After a call: `global.get S; i32.const w; i32.sub; global.set S`. +fn stack_out(out: &mut Vec, g: &Globals, w: u32) { + out.push(0x23); + leb_u(out, g.stack as u64); + out.push(0x41); + leb_s(out, w as i64); + out.extend_from_slice(&[0x6b, 0x24]); + leb_u(out, g.stack as u64); +} + +/// `(func (param f32) (result f32))`: the argument, or the canonical NaN if it is a NaN. +const CANON32: &[u8] = &[0x00, 0x20, 0x00, 0x20, 0x00, 0x5c, 0x04, 0x7d, 0x43, 0x00, 0x00, 0xc0, 0x7f, 0x05, 0x20, 0x00, 0x0b, 0x0b]; +/// `(func (param f64) (result f64))`, likewise. +const CANON64: &[u8] = &[0x00, 0x20, 0x00, 0x20, 0x00, 0x62, 0x04, 0x7c, 0x44, 0, 0, 0, 0, 0, 0, 0xf8, 0x7f, 0x05, 0x20, 0x00, 0x0b, 0x0b]; + +/// The prepared module of a module that passes [`profile::check`] (§4). +pub fn prepare(bytes: &[u8]) -> Result, Refusal> { + prepare_with(bytes, Parts::ALL) +} + +/// [`prepare`] with parts left out: only the tests that show each part is needed use it. +pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { + let facts = profile::check(bytes)?; + let err = |e: wasmparser::BinaryReaderError| Refusal::new("W001", e.message().to_string()); + let mut raw: [Option<&[u8]>; 14] = [None; 14]; + let mut types: Vec = Vec::new(); // parameter count of each type + let mut func_types: Vec = Vec::new(); + let mut n_globals = 0u32; + // Each body: its bytes, where its instructions start, the locals it declares, and its + // instructions with where each starts and ends. + let mut bodies = Vec::new(); + for payload in profile::parser().parse_all(bytes) { + let payload = payload.map_err(err)?; + if let Some((id, range)) = payload.as_section() + && (id as usize) < raw.len() + && id != 0 + { + raw[id as usize] = Some(&bytes[range.start as usize..range.end as usize]); + } + match payload { + Payload::TypeSection(r) => { + for t in r.into_iter_err_on_gc_types() { + types.push(t.map_err(err)?.params().len() as u32); + } + } + Payload::FunctionSection(r) => { + for t in r { + func_types.push(t.map_err(err)?); + } + } + Payload::GlobalSection(r) => n_globals = r.count(), + Payload::CodeSectionEntry(body) => { + let range = body.range(); + let whole = &bytes[range.start as usize..range.end as usize]; + let mut locals = body.get_locals_reader().map_err(err)?; + let mut declared = 0u64; + for _ in 0..locals.get_count() { + declared += locals.read().map_err(err)?.0 as u64; + } + let ops_at = (locals.original_position() - range.start) as usize; + let mut ops = body.get_operators_reader().map_err(err)?; + let mut list = Vec::new(); + while !ops.eof() { + let start = ops.original_position(); + let op = ops.read().map_err(err)?; + list.push((op, (start - range.start) as usize, (ops.original_position() - range.start) as usize)); + } + bodies.push((whole, ops_at, declared as u32, list)); + } + _ => {} + } + } + let g = Globals { fuel: n_globals, stack: n_globals + 1 }; + let n_funcs = func_types.len() as u32; + // The two canonicalising functions, when an instruction of §4.5 is there to use them. + let helpers = parts.nan && bodies.iter().any(|(.., list)| list.iter().any(|(op, ..)| nan_result(op).is_some())); + let (canon32, canon64) = (n_funcs, n_funcs + 1); + + // The function bodies. + let mut code = Vec::new(); + leb_u(&mut code, (bodies.len() + if helpers { 2 } else { 0 }) as u64); + for (k, (whole, ops_at, declared, list)) in bodies.iter().enumerate() { + let params = types[func_types[k] as usize]; + let weight_here = facts.weights[k]; + let decls = &whole[..*ops_at]; + let needs_t = parts.fuel && list.iter().any(|(op, ..)| variable(op)); + let t = params + declared; + let mut f = Vec::new(); + if needs_t { + let (n, at) = read_leb(decls); + leb_u(&mut f, n + 1); + f.extend_from_slice(&decls[at..]); + f.extend_from_slice(&[0x01, 0x7f]); + } else { + f.extend_from_slice(decls); + } + let mut i = 0; + while i < list.len() { + // A run: from here through the first instruction that ends one. The first run + // of a body also carries the function's entry (§4.2). + let mut j = i; + let mut cost = if i == 0 { ENTRY } else { 0 }; + loop { + cost += weight(&list[j].0); + if ends_run(&list[j].0) || j + 1 == list.len() { + break; + } + j += 1; + } + if parts.fuel && cost > 0 { + charge_const(&mut f, &g, cost); + } + for (op, a, b) in &list[i..=j] { + if parts.fuel && variable(op) { + charge_top(&mut f, &g, t, if matches!(op, Operator::MemoryGrow { .. }) { PAGE_FUEL } else { 1 }); + } + let call = matches!(op, Operator::Call { .. } | Operator::CallIndirect { .. }); + if parts.stack && call { + stack_in(&mut f, &g, weight_here); + } + f.extend_from_slice(&whole[*a..*b]); + if parts.stack && call { + stack_out(&mut f, &g, weight_here); + } + if helpers && let Some(wide) = nan_result(op) { + f.push(0x10); + leb_u(&mut f, if wide { canon64 } else { canon32 } as u64); + } + } + i = j + 1; + } + leb_u(&mut code, f.len() as u64); + code.extend_from_slice(&f); + } + if helpers { + for h in [CANON32, CANON64] { + leb_u(&mut code, h.len() as u64); + code.extend_from_slice(h); + } + } + + let mut out = b"\0asm\x01\0\0\0".to_vec(); + let n_types = types.len() as u32; + for id in [1u8, 2, 3, 4, 5, 13, 6, 7, 8, 9, 12, 10, 11] { + match id { + 1 if helpers => section(&mut out, 1, &appended(raw[1], 2, &[0x60, 0x01, 0x7d, 0x01, 0x7d, 0x60, 0x01, 0x7c, 0x01, 0x7c])), + 3 if helpers => { + let mut more = Vec::new(); + leb_u(&mut more, n_types as u64); + leb_u(&mut more, n_types as u64 + 1); + section(&mut out, 3, &appended(raw[3], 2, &more)); + } + 6 => section(&mut out, 6, &appended(raw[6], 2, &[0x7e, 0x01, 0x42, 0x00, 0x0b, 0x7f, 0x01, 0x41, 0x00, 0x0b])), + 7 => { + let mut more = Vec::new(); + for (name, idx) in [(FUEL, g.fuel), (STACK, g.stack)] { + leb_u(&mut more, name.len() as u64); + more.extend_from_slice(name.as_bytes()); + more.push(0x03); + leb_u(&mut more, idx as u64); + } + section(&mut out, 7, &appended(raw[7], 2, &more)); + } + 10 if !bodies.is_empty() || helpers => section(&mut out, 10, &code), + 10 => {} + _ => { + if let Some(r) = raw[id as usize] { + section(&mut out, id, r); + } + } + } + } + // The prepared module is itself a module in the feature set. The profile's limits + // (§3.2) leave room for what preparing adds, so this holds for every module that + // passes; were it not to, the module is refused as past a limit, not run. + let mut v = wasmparser::Validator::new_with_features(profile::features()); + v.validate_all(&out).map_err(|e| Refusal::new("W010", format!("the module is past an engine's limit once prepared: {}", e.message())))?; + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn leb_round_trips() { + for v in [0u64, 1, 127, 128, 300, u32::MAX as u64, u64::MAX] { + let mut b = Vec::new(); + leb_u(&mut b, v); + assert_eq!(read_leb(&b), (v, b.len())); + } + let mut b = Vec::new(); + leb_s(&mut b, -1); + assert_eq!(b, [0x7f]); + b.clear(); + leb_s(&mut b, 64); + assert_eq!(b, [0xc0, 0x00]); + } + + /// The largest growth preparing can cause: a body at the profile's limit that is all + /// calls, with enough globals, functions and types that every added index takes three + /// bytes, and a float so the two functions are added. (`tests/engines.rs` compiles and + /// runs a module at the profile's limits on every engine.) + #[test] + fn a_module_at_every_limit_prepares_within_an_engine_s_limits() { + use crate::profile::MAX_BODY_BYTES; + let leb = |v: u64| { + let mut b = Vec::new(); + leb_u(&mut b, v); + b + }; + let sec = |id: u8, body: Vec| { + let mut s = vec![id]; + s.extend(leb(body.len() as u64)); + s.extend(body); + s + }; + // Past 2^14 of each, so that the indices preparing adds take three bytes. + let n = (1u64 << 14) + 2; + let mut types = leb(n); + for _ in 0..n { + types.extend_from_slice(&[0x60, 0x00, 0x00]); + } + let mut funcs = leb(n); + funcs.extend(std::iter::repeat_n(0u8, n as usize)); + let mut globals = leb(n); + for _ in 0..n { + globals.extend_from_slice(&[0x7f, 0x00, 0x41, 0x00, 0x0b]); + } + // The first body: `f64.const 0; f64.sqrt; drop`, then calls of function 1 up to the limit. + let mut big = vec![0x00, 0x44, 0, 0, 0, 0, 0, 0, 0, 0, 0x9f, 0x1a]; + while (big.len() as u64) + 3 <= MAX_BODY_BYTES { + big.extend_from_slice(&[0x10, 0x01]); + } + big.push(0x0b); + let mut code = leb(n); + code.extend(leb(big.len() as u64)); + code.extend_from_slice(&big); + for _ in 0..n - 1 { + code.extend_from_slice(&[0x02, 0x00, 0x0b]); + } + let mut m = b"\0asm\x01\0\0\0".to_vec(); + for s in [sec(1, types), sec(3, funcs), sec(6, globals), sec(10, code)] { + m.extend(s); + } + profile::check(&m).unwrap(); + let p = prepare(&m).unwrap(); + // The body grew about 53 times, each call of two bytes metered and weighed with + // indices of three, and the module is still one an engine takes whole + // (`tests/engines.rs` compiles one). + let growth = (p.len() - m.len()) as f64 / big.len() as f64; + assert!(growth > 40.0 && growth < 64.0 && p.len() < 2 << 20, "{} bytes, the body {growth:.1} times", p.len()); + } + + #[test] + fn preparing_is_a_function_of_the_bytes_and_keeps_every_instruction() { + let m = wat::parse_str( + r#"(module (memory 1 2) (func (export "f") (param i32) (result i32) + (local i32) local.get 0 if (result i32) i32.const 1 else i32.const 2 end + i32.const 0 i32.const 0 i32.const 8 memory.copy))"#, + ) + .unwrap(); + let a = prepare(&m).unwrap(); + assert_eq!(a, prepare(&m).unwrap()); + // The original's instructions all appear, in order, in each prepared body. + let bodies = |bytes: &[u8]| -> Vec> { + let mut out = Vec::new(); + for payload in wasmparser::Parser::new(0).parse_all(bytes) { + if let wasmparser::Payload::CodeSectionEntry(body) = payload.unwrap() { + let mut ops = body.get_operators_reader().unwrap(); + let mut v = Vec::new(); + while !ops.eof() { + v.push(format!("{:?}", ops.read().unwrap())); + } + out.push(v); + } + } + out + }; + let (was, now) = (bodies(&m), bodies(&a)); + assert!(now.len() >= was.len()); + for (k, (w, n)) in was.iter().zip(&now).enumerate() { + let mut rest = n.iter(); + assert!(w.iter().all(|op| rest.any(|x| x == op)), "function {k}: {w:?} is not in order in {n:?}"); + assert!(n.len() > w.len(), "function {k} is metered"); + } + assert!(profile::check(&a).is_err(), "the prepared module exports the reserved names"); + } +} diff --git a/wai/wai-wasmq/src/profile.rs b/wai/wai-wasmq/src/profile.rs new file mode 100644 index 000000000..41029d749 --- /dev/null +++ b/wai/wai-wasmq/src/profile.rs @@ -0,0 +1,624 @@ +//! The profile a module is held to before anything runs it (extensions/quantum-wasmq.md §3): +//! a core WebAssembly module in the Lime1 feature set (WebAssembly tool conventions), with +//! nothing it can reach outside itself and nothing whose result a host may choose. +//! +//! [`check`] reads a module and returns what the rest of the crate needs to know about it +//! ([`Facts`]), or a rule it breaks ([`Refusal`]), each rule with a stable code. It reads +//! in two passes. The first ([`scan`]) reads the module with every feature a reader knows +//! and no validator, and refuses what the profile names: an import, a memory it does not +//! take, an instruction or a type of a feature outside Lime1, a size past a limit. The +//! second validates what is left strictly under Lime1, with the parser held to the same +//! features, and anything it refuses is `W001`. Neither pass panics on any bytes. + +use std::collections::BTreeMap; +use wasmparser::{ + CompositeInnerType, ConstExpr, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, Payload, TypeRef, ValType, Validator, + WasmFeatures, +}; + +/// The profile's name: the engine's (`engine::wasmq`), where every verifier reads it. +pub use wai_quantum::engine::wasmq::PROFILE; + +/// The most 64 KiB pages a module may declare as its memory's maximum (256 MiB). +pub const MAX_MEMORY_PAGES: u64 = 4096; +/// The most bytes a module may have (512 KiB): what a bundle's modules hold among them. +pub const MAX_MODULE_BYTES: usize = 1 << 19; +/// The most types, functions, globals, data segments and element segments a module may +/// have, each. +pub const MAX_ITEMS: u32 = 1 << 16; +/// The most exports a module may have. +pub const MAX_EXPORTS: u32 = 1 << 10; +/// The most bytes an export's name may have. +pub const MAX_NAME_BYTES: usize = 1 << 8; +/// The most parameters, and the most results, a function type may have. +pub const MAX_TYPE_VALUES: usize = 1 << 8; +/// The most bytes one function's entry in the code section may have (16 KiB): prepared, a +/// body grows up to about 53 times, and an engine compiles a function whole. +pub const MAX_BODY_BYTES: u64 = 1 << 14; +/// The most locals and operands one function's frame may hold at once. +pub const MAX_FRAME_SLOTS: u32 = 4096; +/// What every frame weighs before its own values (§4.3). +pub const FRAME_BASE: u32 = 576; +/// The most the frames suspended at a call may weigh together inside one host call (§4.3). +pub const MAX_STACK: u32 = 1 << 23; +/// The most entries the table may have. +pub const MAX_TABLE: u64 = 1 << 16; +/// Export names the prepared module adds (§4.4); a module may not use the prefix. +pub const RESERVED_PREFIX: &str = "__wasmq"; +/// The name a module exports its memory under, where a host reaches it (§5.2). +pub const MEMORY: &str = "memory"; + +/// A rule a module breaks, with its code (§3.3). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Refusal { + pub code: &'static str, + pub message: String, +} + +impl Refusal { + pub fn new(code: &'static str, message: impl Into) -> Refusal { + Refusal { code, message: message.into() } + } +} + +impl std::fmt::Display for Refusal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}", self.code, self.message) + } +} + +/// The refusal codes, each with what it means: the table of §3.3. +pub const CODES: &[(&str, &str)] = &[ + ("W001", "not a valid core module in the Lime1 feature set, for a reason no other code names (a component, a malformed module, a function body that does not end)"), + ("W002", "the module imports something: a wasmQ module imports nothing"), + ("W003", "the memory: more than one, 64-bit, a page size of its own, without a maximum, a maximum past the profile's, or (envelope) more among a bundle's modules than one may have"), + ("W004", "a start function: nothing runs when a module is instantiated"), + ("W005", "threads: a shared memory, global, table or type, or an atomic instruction"), + ("W006", "a vector (SIMD) instruction or type, relaxed or not"), + ("W007", "an instruction or a type of a feature outside Lime1 that no other code names: references, exceptions, tail calls, garbage collection, passive segments and the rest"), + ("W008", "the table: more than one, a type other than funcref, an instruction that reads or changes it other than call_indirect, or larger than the profile allows"), + ("W009", "a function whose locals and operands can pass the profile's frame"), + ("W010", "larger than the profile allows: the module, a function's body, a function type, an export's name, or the number of its types, functions, globals, exports or segments"), + ("W011", "a data or element segment that does not fit where it is placed"), + ("W012", "an export whose name the profile reserves, or an export a host calls (a lifecycle export, cabi_realloc, cabi_post_*) with another type than its own"), + ("W013", "the envelope: not a WAI1 envelope of this capability, past the profile's size, a contract that is not canonical or holds other members than the profile's, or frames that do not add up"), + ("W014", "a frame whose digest is not the one the contract pins: the program, a module, or a prior"), + ("W015", "the callouts: not one for each `extern` in the order declared, a module no callout names, an export that is not the type its `extern` lowers to, or a module the host must write to that does not export its memory and cabi_realloc (§5.2)"), + ("W016", "the program: the reader refuses it, it calls nothing, it has a gate outside the Clifford group, more qubits than the method holds, or more steps than the profile's"), + ("W017", "the run: shots, seed, an input or a fuel budget outside what the profile and a receipt can hold, or results or work on the host's part past the profile's"), +]; + +/// What the rest of the crate needs to know about a module that passes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Facts { + /// Each exported function by name: its parameter and result types. + pub functions: BTreeMap, Vec)>, + /// The memory's initial and maximum pages, if it has one. + pub memory: Option<(u64, u64)>, + /// Whether the memory is exported as `memory`, where a host reaches it. + pub memory_exported: bool, + /// Whether any type, local, global or instruction is of a floating-point type. + pub floats: bool, + /// The frame weight of each function, by its index (§4.3). + pub weights: Vec, +} + +fn has_float(ty: &ValType) -> bool { + matches!(ty, ValType::F32 | ValType::F64) +} + +/// The Lime1 feature set: WebAssembly 1.0 with mutable globals, multi-value, sign +/// extension, saturating float-to-int, `memory.copy` and `memory.fill`, extended constant +/// expressions, and the overlong `call_indirect` encoding. +pub fn features() -> WasmFeatures { + WasmFeatures::LIME1 +} + +/// A parser held to the profile's features, so that an encoding only another feature +/// gives meaning to is refused where it is read. +pub fn parser() -> Parser { + let mut p = Parser::new(0); + p.set_features(features()); + p +} + +/// The weight of a function's frame (§4.3): a base, and one for each parameter, result +/// and local, for each operand its body can hold at once (a call's results among them), +/// and for each byte of its entry in the code section. +pub fn frame_weight(params: u32, results: u32, locals: u32, operands: u32, body_bytes: u64) -> u32 { + FRAME_BASE + params + results + locals + operands + body_bytes as u32 +} + +/// The type an export a host calls must have (§5.2, §5.3), by its name. +pub fn host_export(name: &str) -> Option<(Vec, Vec)> { + use ValType::{I32, I64}; + match name { + "wasmq-seed" => Some((vec![I64], vec![])), + "wasmq-prior" => Some((vec![I32, I32, I32], vec![])), + "init" => Some((vec![], vec![])), + "cabi_realloc" => Some((vec![I32, I32, I32, I32], vec![I32])), + n if n.starts_with("cabi_post_") => Some((vec![I32], vec![])), + _ => None, + } +} + +// The feature each instruction belongs to, from the reader's own table. +macro_rules! feature_of { + ($(@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => { + #[allow(unreachable_patterns)] + fn feature(op: &Operator<'_>) -> &'static str { + match op { + $(Operator::$op { .. } => stringify!($proposal),)* + _ => "unknown", + } + } + }; +} +wasmparser::for_each_operator!(feature_of); + +/// The code an instruction is refused under, if the profile does not take it (§3.2). +fn instruction_rule(op: &Operator<'_>) -> Option<&'static str> { + use Operator::*; + match op { + MemoryCopy { .. } | MemoryFill { .. } => None, + TableGet { .. } | TableSet { .. } | TableGrow { .. } | TableSize { .. } | TableFill { .. } | TableCopy { .. } | TableInit { .. } | ElemDrop { .. } => Some("W008"), + _ => match feature(op) { + "mvp" | "sign_extension" | "saturating_float_to_int" => None, + "threads" | "shared_everything_threads" => Some("W005"), + "simd" | "relaxed_simd" => Some("W006"), + _ => Some("W007"), + }, + } +} + +/// The code a value type is refused under, if the profile does not take it. +fn type_rule(ty: &ValType) -> Option<&'static str> { + match ty { + ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64 => None, + ValType::V128 => Some("W006"), + ValType::Ref(_) => Some("W007"), + } +} + +fn malformed(e: wasmparser::BinaryReaderError) -> Refusal { + Refusal::new("W001", e.message().to_string()) +} + +/// The first pass: what the profile names, read with every feature and no validator. +fn scan(bytes: &[u8]) -> Result<(), Refusal> { + let count = |n: u32, what: &str, max: u32| if n > max { Err(Refusal::new("W010", format!("{n} {what}; the profile takes at most {max}"))) } else { Ok(()) }; + let mut memories = 0; + let mut tables = 0; + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(malformed)? { + Payload::Version { encoding: wasmparser::Encoding::Component, .. } => { + return Err(Refusal::new("W001", "a component; wasmQ takes a core module, which a component host may wrap (§3.1)")); + } + Payload::TypeSection(r) => { + count(r.count(), "types", MAX_ITEMS)?; + for group in r { + let group = group.map_err(malformed)?; + if group.is_explicit_rec_group() { + return Err(Refusal::new("W007", "a recursive type group (garbage collection)")); + } + for sub in group.types() { + if sub.composite_type.shared { + return Err(Refusal::new("W005", "a shared type")); + } + let CompositeInnerType::Func(f) = &sub.composite_type.inner else { + return Err(Refusal::new("W007", "a type that is not a function type (garbage collection, stack switching)")); + }; + if !sub.is_final || !sub.supertype_idxs.is_empty() { + return Err(Refusal::new("W007", "a function type with a supertype (garbage collection)")); + } + if let Some(code) = f.params().iter().chain(f.results()).find_map(type_rule) { + return Err(Refusal::new(code, "a function type of a value that is not a number")); + } + if f.params().len() > MAX_TYPE_VALUES || f.results().len() > MAX_TYPE_VALUES { + return Err(Refusal::new("W010", format!("a function type of {} parameters and {} results; the profile takes at most {MAX_TYPE_VALUES} of each", f.params().len(), f.results().len()))); + } + } + } + } + Payload::ImportSection(r) => { + if let Some(i) = r.into_imports().next() { + let i = i.map_err(malformed)?; + let what = match i.ty { + TypeRef::Func(_) | TypeRef::FuncExact(_) => "a function", + TypeRef::Memory(_) => "a memory", + TypeRef::Table(_) => "a table", + TypeRef::Global(_) => "a global", + TypeRef::Tag(_) => "a tag", + }; + return Err(Refusal::new("W002", format!("it imports {what}, {}.{}: a wasmQ module imports nothing, so nothing outside it (a clock, randomness, the network) can reach its result", i.module, i.name))); + } + } + Payload::FunctionSection(r) => count(r.count(), "functions", MAX_ITEMS)?, + Payload::StartSection { .. } => return Err(Refusal::new("W004", "a start function; nothing runs when the module is instantiated (§4.1)")), + Payload::MemorySection(r) => { + for m in r { + let m = m.map_err(malformed)?; + memories += 1; + if memories > 1 { + return Err(Refusal::new("W003", "more than one memory")); + } + if m.shared { + return Err(Refusal::new("W005", "a shared memory")); + } + if m.memory64 { + return Err(Refusal::new("W003", "a 64-bit memory")); + } + if m.page_size_log2.is_some() { + return Err(Refusal::new("W003", "a memory with a page size of its own")); + } + match m.maximum { + None => return Err(Refusal::new("W003", "a memory without a maximum: it could grow as far as a host lets it, which differs by host")), + Some(max) if max > MAX_MEMORY_PAGES => { + return Err(Refusal::new("W003", format!("a memory of up to {max} pages; the profile takes at most {MAX_MEMORY_PAGES}"))); + } + Some(_) => {} + } + } + } + Payload::TableSection(r) => { + for t in r { + let t = t.map_err(malformed)?; + tables += 1; + if tables > 1 { + return Err(Refusal::new("W008", "more than one table")); + } + if t.ty.shared { + return Err(Refusal::new("W005", "a shared table")); + } + if t.ty.element_type != wasmparser::RefType::FUNCREF || t.ty.table64 || !matches!(t.init, wasmparser::TableInit::RefNull) { + return Err(Refusal::new("W008", "a table that is not a 32-bit table of funcref, empty until its segments fill it")); + } + let max = t.ty.maximum.unwrap_or(t.ty.initial).max(t.ty.initial); + if max > MAX_TABLE { + return Err(Refusal::new("W008", format!("a table of up to {max} entries; the profile takes at most {MAX_TABLE}"))); + } + } + } + Payload::TagSection(_) => return Err(Refusal::new("W007", "a tag (exception handling)")), + Payload::GlobalSection(r) => { + count(r.count(), "globals", MAX_ITEMS)?; + for g in r { + let g = g.map_err(malformed)?; + if g.ty.shared { + return Err(Refusal::new("W005", "a shared global")); + } + if let Some(code) = type_rule(&g.ty.content_type) { + return Err(Refusal::new(code, "a global that is not a number")); + } + } + } + Payload::ExportSection(r) => { + count(r.count(), "exports", MAX_EXPORTS)?; + for e in r { + let n = e.map_err(malformed)?.name.len(); + if n > MAX_NAME_BYTES { + return Err(Refusal::new("W010", format!("an export's name of {n} bytes; the profile takes at most {MAX_NAME_BYTES}"))); + } + } + } + Payload::ElementSection(r) => count(r.count(), "element segments", MAX_ITEMS)?, + Payload::DataSection(r) => count(r.count(), "data segments", MAX_ITEMS)?, + Payload::CodeSectionEntry(body) => { + let size = body.range().end - body.range().start; + if size > MAX_BODY_BYTES { + return Err(Refusal::new("W010", format!("a function body of {size} bytes; the profile takes at most {MAX_BODY_BYTES}"))); + } + // The body's locals and instructions, as far as they read: what does not + // read is the second pass's to refuse. + let Ok(mut locals) = body.get_locals_reader() else { continue }; + let mut read = true; + for _ in 0..locals.get_count() { + match locals.read() { + Ok((_, ty)) => { + if let Some(code) = type_rule(&ty) { + return Err(Refusal::new(code, "a local that is not a number")); + } + } + Err(_) => { + read = false; + break; + } + } + } + let Ok(mut ops) = body.get_operators_reader() else { continue }; + while read && !ops.eof() { + let Ok((op, off)) = ops.read_with_offset() else { break }; + if let Some(code) = instruction_rule(&op) { + let name = format!("{op:?}"); + let name = name.split(|c: char| !c.is_ascii_alphanumeric()).next().unwrap_or(""); + return Err(Refusal::new(code, format!("{name} at offset {off}: an instruction of a feature outside Lime1"))); + } + } + } + _ => {} + } + } + Ok(()) +} + +/// Reads `bytes` as a wasmQ module: its facts, or a rule it breaks. A module that breaks +/// more than one rule is refused for one of them. +pub fn check(bytes: &[u8]) -> Result { + if bytes.len() > MAX_MODULE_BYTES { + return Err(Refusal::new("W010", format!("{} bytes; the profile takes at most {MAX_MODULE_BYTES}", bytes.len()))); + } + scan(bytes)?; + let mut v = Validator::new_with_features(features()); + let mut facts = Facts { functions: BTreeMap::new(), memory: None, memory_exported: false, floats: false, weights: Vec::new() }; + let mut types: Vec = Vec::new(); + let mut func_types: Vec = Vec::new(); + let mut table_min: Option = None; + let mut globals: Vec<(ValType, Option)> = Vec::new(); + let mut allocs = wasmparser::FuncValidatorAllocations::default(); + for payload in parser().parse_all(bytes) { + let payload = payload.map_err(malformed)?; + match v.payload(&payload).map_err(malformed)? { + wasmparser::ValidPayload::Func(func, body) => { + let mut fv = func.into_validator(std::mem::take(&mut allocs)); + let idx = fv.index(); + let ty = types.get(*func_types.get(idx as usize).ok_or_else(|| Refusal::new("W001", "a body without a function"))? as usize).ok_or_else(|| Refusal::new("W001", "a function without a type"))?; + let (params, results) = (ty.params().len() as u64, ty.results().len() as u32); + let mut locals = 0u64; + let mut lr = body.get_locals_reader().map_err(malformed)?; + for _ in 0..lr.get_count() { + let off = lr.original_position(); + let (n, ty) = lr.read().map_err(malformed)?; + facts.floats |= has_float(&ty); + locals += n as u64; + if params + locals > MAX_FRAME_SLOTS as u64 { + return Err(Refusal::new("W009", format!("function {idx} declares {} parameters and locals; a frame holds at most {MAX_FRAME_SLOTS} locals and operands", params + locals))); + } + fv.define_locals(off, n, ty).map_err(malformed)?; + } + let mut ops = body.get_operators_reader().map_err(malformed)?; + let mut operands = 0u32; + while !ops.eof() { + let (op, off) = ops.read_with_offset().map_err(malformed)?; + facts.floats |= float_op(&op); + fv.op(off, &op).map_err(malformed)?; + operands = operands.max(fv.operand_stack_height()); + let frame = params + locals + fv.operand_stack_height() as u64; + if frame > MAX_FRAME_SLOTS as u64 { + return Err(Refusal::new("W009", format!("function {idx} holds {frame} locals and operands at offset {off}; a frame holds at most {MAX_FRAME_SLOTS}"))); + } + } + // The body ends where its last block does, with nothing after it. + ops.finish().map_err(malformed)?; + allocs = fv.into_allocations(); + let size = body.range().end - body.range().start; + facts.weights.push(frame_weight(params as u32, results, locals as u32, operands, size)); + } + wasmparser::ValidPayload::End(_) => {} + _ => {} + } + match payload { + Payload::TypeSection(r) => { + for rec in r.into_iter_err_on_gc_types() { + let t = rec.map_err(malformed)?; + facts.floats |= t.params().iter().chain(t.results()).any(has_float); + types.push(t); + } + } + Payload::FunctionSection(r) => { + for t in r { + func_types.push(t.map_err(malformed)?); + } + } + Payload::TableSection(r) => { + for t in r { + table_min = Some(t.map_err(malformed)?.ty.initial); + } + } + Payload::MemorySection(r) => { + for m in r { + let m = m.map_err(malformed)?; + facts.memory = Some((m.initial, m.maximum.unwrap_or(m.initial))); + } + } + Payload::GlobalSection(r) => { + for g in r { + let g = g.map_err(malformed)?; + facts.floats |= has_float(&g.ty.content_type); + let val = const_i64(&g.init_expr, &globals); + globals.push((g.ty.content_type, if g.ty.mutable { None } else { val })); + } + } + Payload::ExportSection(r) => { + for e in r { + let e = e.map_err(malformed)?; + if e.name.starts_with(RESERVED_PREFIX) { + return Err(Refusal::new("W012", format!("the export {:?}: names that begin {RESERVED_PREFIX} are the prepared module's (§4.4)", e.name))); + } + match e.kind { + ExternalKind::Func | ExternalKind::FuncExact => { + let t = func_types.get(e.index as usize).and_then(|t| types.get(*t as usize)).ok_or_else(|| Refusal::new("W001", "an export of a function the module does not have"))?; + let got = (t.params().to_vec(), t.results().to_vec()); + if let Some(want) = host_export(e.name) + && got != want + { + return Err(Refusal::new("W012", format!("the export {:?} is {:?} -> {:?}; a host calls it as {:?} -> {:?}", e.name, got.0, got.1, want.0, want.1))); + } + facts.functions.insert(e.name.to_string(), got); + } + ExternalKind::Memory if e.name == MEMORY => facts.memory_exported = true, + _ => {} + } + } + } + Payload::DataSection(r) => { + let size = facts.memory.map(|(min, _)| min << 16).unwrap_or(0); + for d in r { + let d = d.map_err(malformed)?; + match d.kind { + DataKind::Active { offset_expr, .. } => { + let at = const_i64(&offset_expr, &globals).ok_or_else(|| Refusal::new("W011", "a data segment placed where only running the module would say"))?; + let end = (at as u32 as u64) + d.data.len() as u64; + if end > size { + return Err(Refusal::new("W011", format!("a data segment ends at byte {end}, past the {size} bytes the memory starts with"))); + } + } + DataKind::Passive => return Err(Refusal::new("W007", "a passive data segment")), + } + } + } + Payload::ElementSection(r) => { + for el in r { + let el = el.map_err(malformed)?; + let n = match el.items { + ElementItems::Functions(f) => f.count() as u64, + ElementItems::Expressions(..) => return Err(Refusal::new("W008", "an element segment of expressions; the profile takes function indices")), + }; + match el.kind { + ElementKind::Active { offset_expr, table_index } => { + if table_index.unwrap_or(0) != 0 { + return Err(Refusal::new("W008", "an element segment for a second table")); + } + let at = const_i64(&offset_expr, &globals).ok_or_else(|| Refusal::new("W011", "an element segment placed where only running the module would say"))?; + let end = (at as u32 as u64) + n; + if end > table_min.unwrap_or(0) { + return Err(Refusal::new("W011", format!("an element segment ends at entry {end}, past the {} the table starts with", table_min.unwrap_or(0)))); + } + } + _ => return Err(Refusal::new("W008", "a passive or declared element segment; the profile fills the table once, from active segments")), + } + } + } + _ => {} + } + } + Ok(facts) +} + +/// The value of a constant expression of `i32.const`, `i64.const`, `global.get` of an +/// immutable global, and the extended-constant `add`, `sub` and `mul`. +pub fn const_i64(e: &ConstExpr<'_>, globals: &[(ValType, Option)]) -> Option { + let mut stack: Vec = Vec::new(); + let mut r = e.get_operators_reader(); + while !r.eof() { + match r.read().ok()? { + Operator::I32Const { value } => stack.push(value as i64), + Operator::I64Const { value } => stack.push(value), + Operator::GlobalGet { global_index } => stack.push(globals.get(global_index as usize)?.1?), + Operator::I32Add => bin(&mut stack, |a, b| (a as i32).wrapping_add(b as i32) as i64)?, + Operator::I32Sub => bin(&mut stack, |a, b| (a as i32).wrapping_sub(b as i32) as i64)?, + Operator::I32Mul => bin(&mut stack, |a, b| (a as i32).wrapping_mul(b as i32) as i64)?, + Operator::I64Add => bin(&mut stack, i64::wrapping_add)?, + Operator::I64Sub => bin(&mut stack, i64::wrapping_sub)?, + Operator::I64Mul => bin(&mut stack, i64::wrapping_mul)?, + Operator::End => {} + _ => return None, + } + } + stack.pop() +} + +fn bin(stack: &mut Vec, f: impl Fn(i64, i64) -> i64) -> Option<()> { + let b = stack.pop()?; + let a = stack.pop()?; + stack.push(f(a, b)); + Some(()) +} + +/// Whether `op` reads or writes a floating-point value. +pub fn float_op(op: &Operator<'_>) -> bool { + use Operator::*; + matches!( + op, + F32Load { .. } | F64Load { .. } | F32Store { .. } | F64Store { .. } | F32Const { .. } | F64Const { .. } + | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge | F64Eq | F64Ne | F64Lt | F64Gt | F64Le | F64Ge + | F32Abs | F32Neg | F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt | F32Add | F32Sub | F32Mul | F32Div | F32Min | F32Max | F32Copysign + | F64Abs | F64Neg | F64Ceil | F64Floor | F64Trunc | F64Nearest | F64Sqrt | F64Add | F64Sub | F64Mul | F64Div | F64Min | F64Max | F64Copysign + | I32TruncF32S | I32TruncF32U | I32TruncF64S | I32TruncF64U | I64TruncF32S | I64TruncF32U | I64TruncF64S | I64TruncF64U + | F32ConvertI32S | F32ConvertI32U | F32ConvertI64S | F32ConvertI64U | F32DemoteF64 + | F64ConvertI32S | F64ConvertI32U | F64ConvertI64S | F64ConvertI64U | F64PromoteF32 + | I32ReinterpretF32 | I64ReinterpretF64 | F32ReinterpretI32 | F64ReinterpretI64 + | I32TruncSatF32S | I32TruncSatF32U | I32TruncSatF64S | I32TruncSatF64U | I64TruncSatF32S | I64TruncSatF32U | I64TruncSatF64S | I64TruncSatF64U + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn code(wat: &str) -> &'static str { + match check(&wat::parse_str(wat).unwrap()) { + Ok(_) => "ok", + Err(r) => r.code, + } + } + + #[test] + fn the_rules_each_refuse_what_they_name() { + assert_eq!(code("(module (memory 1 1) (func (export \"f\") (result i32) i32.const 1))"), "ok"); + assert_eq!(code("(module (import \"wasi\" \"clock_time_get\" (func)) )"), "W002"); + assert_eq!(code("(module (memory 1))"), "W003"); + assert_eq!(code("(module (memory 1 5000))"), "W003"); + assert_eq!(code("(module (memory 1 1) (memory 1 1))"), "W003"); + assert_eq!(code("(module (func $s) (start $s))"), "W004"); + assert_eq!(code("(module (memory 1 1 shared))"), "W005"); + assert_eq!(code("(module (memory 1 1) (func (result i32) i32.const 0 i32.const 1 i32.atomic.rmw.add))"), "W005"); + assert_eq!(code("(module (func (result v128) v128.const i64x2 0 0))"), "W006"); + assert_eq!(code("(module (func (param v128) (result v128) local.get 0 local.get 0 local.get 0 f32x4.relaxed_madd))"), "W006"); + assert_eq!(code("(module (table 1 funcref) (func (result i32) table.size 0))"), "W008"); + assert_eq!(code("(module (table 1 funcref) (func unreachable table.grow 0 drop))"), "W008"); + // Two rules broken: the reference instruction comes first in the body. + assert_eq!(code("(module (table 1 funcref) (func (result i32) ref.null func i32.const 1 table.grow 0))"), "W007"); + assert_eq!(code("(module (func (result i32) ref.null func ref.is_null))"), "W007"); + assert_eq!(code("(module (func $f) (func return_call $f))"), "W007"); + assert_eq!(code("(module (memory 1 1) (data \"passive\") (func i32.const 0 i32.const 0 i32.const 1 memory.init 0))"), "W007"); + assert_eq!(code("(module (func (result i32) i32.const 1 i32.const 2 i32.add i32.add))"), "W001"); + assert_eq!(code("(module (memory 1 1) (data (i32.const 65530) \"0123456789\"))"), "W011"); + assert_eq!(code("(module (func (export \"__wasmq_fuel\")))"), "W012"); + assert_eq!(code("(component)"), "W001"); + } + + #[test] + fn a_frame_past_the_profile_is_refused() { + let wide = format!("(module (func (local {}) ))", "i32 ".repeat(MAX_FRAME_SLOTS as usize + 1)); + assert_eq!(code(&wide), "W009"); + let fits = format!("(module (func (local {}) ))", "i32 ".repeat(MAX_FRAME_SLOTS as usize - 1)); + assert_eq!(code(&fits), "ok"); + // Operands count too: a frame of 4000 locals may not also hold 200 operands. + let deep = format!("(module (func (local {}) {} {}))", "i32 ".repeat(4000), "i32.const 0 ".repeat(200), "drop ".repeat(200)); + assert_eq!(code(&deep), "W009"); + } + + #[test] + fn each_limit_admits_its_largest_and_refuses_one_more() { + let params = |n: usize| format!("(module (func (param {})))", "i64 ".repeat(n)); + assert_eq!((code(¶ms(MAX_TYPE_VALUES)), code(¶ms(MAX_TYPE_VALUES + 1))), ("ok", "W010")); + let results = |n: usize| format!("(module (func (result {}) unreachable))", "i64 ".repeat(n)); + assert_eq!((code(&results(MAX_TYPE_VALUES)), code(&results(MAX_TYPE_VALUES + 1))), ("ok", "W010")); + let exports = |n: u32| format!("(module (func) {})", (0..n).map(|k| format!("(export \"e{k}\" (func 0)) ")).collect::()); + assert_eq!((code(&exports(MAX_EXPORTS)), code(&exports(MAX_EXPORTS + 1))), ("ok", "W010")); + let named = |n: usize| format!("(module (func (export \"{}\")))", "n".repeat(n)); + assert_eq!((code(&named(MAX_NAME_BYTES)), code(&named(MAX_NAME_BYTES + 1))), ("ok", "W010")); + let globals = |n: u32| format!("(module {})", "(global i32 (i32.const 0)) ".repeat(n as usize)); + assert_eq!((code(&globals(MAX_ITEMS)), code(&globals(MAX_ITEMS + 1))), ("ok", "W010")); + // A body's size is its entry in the code section: one byte of locals, `nop`s, `end`. + let body = |bytes: u64| format!("(module (func {}))", "nop ".repeat(bytes as usize - 2)); + assert_eq!((code(&body(MAX_BODY_BYTES)), code(&body(MAX_BODY_BYTES + 1))), ("ok", "W010")); + let mut big = wat::parse_str("(module)").unwrap(); + big.extend_from_slice(&[0x00, 0x80, 0x80, 0x80, 0x02, 0x01, b'x']); + big.resize(MAX_MODULE_BYTES + 1, 0); + assert_eq!(check(&big).unwrap_err().code, "W010"); + } + + #[test] + fn floats_are_noticed_and_frames_are_weighed() { + assert!(!check(&wat::parse_str("(module (func (result i32) i32.const 1))").unwrap()).unwrap().floats); + assert!(check(&wat::parse_str("(module (func (result f64) f64.const 1))").unwrap()).unwrap().floats); + // `(func (param i64 i64) (result i64) (local i32) local.get 0)`: an entry of one + // local group (3 bytes), `local.get 0` (2) and `end` (1); one operand at most. + let f = check(&wat::parse_str("(module (func (param i64 i64) (result i64) (local i32) local.get 0))").unwrap()).unwrap(); + assert_eq!(f.weights, vec![FRAME_BASE + 2 + 1 + 1 + 1 + 6]); + // A call's results count where they are held: three results of one call of two bytes. + let f = check(&wat::parse_str("(module (func $p (result i64 i64 i64) i64.const 1 i64.const 2 i64.const 3) (func call $p drop drop drop))").unwrap()).unwrap(); + assert_eq!(f.weights[1], FRAME_BASE + 3 + 7); + } +} diff --git a/wai/wai-wasmq/src/reference.rs b/wai/wai-wasmq/src/reference.rs new file mode 100644 index 000000000..b9313a40d --- /dev/null +++ b/wai/wai-wasmq/src/reference.rs @@ -0,0 +1,132 @@ +//! The reference programs (extensions/quantum-wasmq.md §8): circuits that call the +//! reference modules in the middle, written as OpenQASM 3, and the priors those modules +//! take. The tests, the conformance vectors and the command line's examples build their +//! bundles from these. + +use crate::envelope::{Bundle, Callout, Module, Run}; +use std::fmt::Write as _; + +/// A code's parity checks in the form the decoder takes as its prior: the bytes +/// `CssCode::check_matrix_bytes` writes. +pub fn checks_prior(n: usize, checks: &[Vec]) -> Vec { + let mut b = b"wai:qec-checks\x01".to_vec(); + b.extend_from_slice(&(n as u64).to_le_bytes()); + for row in checks { + b.extend_from_slice(&(row.len() as u32).to_le_bytes()); + for &c in row { + b.extend_from_slice(&(c as u32).to_le_bytes()); + } + } + b +} + +/// A target register in the form the planner takes as its prior: the bytes +/// `quantum_atom::target_bytes` writes. +pub fn target_prior(w: u32, h: u32, target: &[usize]) -> Vec { + let mut b = b"wai:qc-target\x01".to_vec(); + b.extend_from_slice(&w.to_le_bytes()); + b.extend_from_slice(&h.to_le_bytes()); + let mut t = target.to_vec(); + t.sort_unstable(); + for s in t { + b.extend_from_slice(&(s as u32).to_le_bytes()); + } + b +} + +/// A centred `tw × th` block of a `w × h` array, as `quantum_atom::target_block` gives it. +pub fn target_block(w: u32, h: u32, tw: u32, th: u32) -> Vec { + let (x0, y0) = ((w - tw) / 2, (h - th) / 2); + (y0..y0 + th).flat_map(|y| (x0..x0 + tw).map(move |x| (y * w + x) as usize)).collect() +} + +fn bits_for(n: usize) -> usize { + (usize::BITS - (n.max(2) - 1).leading_zeros()) as usize +} + +/// A memory round of a code with parity checks `checks` on `n` qubits, its decoding a +/// callout in the middle of the circuit: `errors` single-qubit X errors at places drawn +/// from coin qubits, the syndrome measured on ancillas, the decoder called on it, its +/// correction applied, the syndrome measured again and the data read out. The callout is +/// `decode64` when the checks and the qubits fit 64 bits, `decode` otherwise. +pub fn qec_program(n: usize, checks: &[Vec], errors: usize) -> String { + let m = checks.len(); + let k = bits_for(n); + let wide = n > 64 || m > 64; + let f = if wide { "decode" } else { "decode64" }; + let mut p = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + let _ = writeln!(p, "// A memory round of a code of {n} qubits and {m} parity checks, decoded in the middle of the circuit."); + let _ = writeln!(p, "extern {f}(bit[{m}]) -> bit[{n}];"); + let _ = writeln!(p, "qubit[{n}] d;\nqubit[{m}] a;"); + for e in 0..errors { + let _ = writeln!(p, "qubit[{k}] c{e};\nbit[{k}] e{e};"); + } + let _ = writeln!(p, "bit[{m}] s;\nbit[{n}] corr;\nbit[{m}] s2;\nbit[{n}] fin;"); + for e in 0..errors { + let _ = writeln!(p, "h c{e};\ne{e} = measure c{e};"); + for i in 0..n { + let _ = writeln!(p, "if (e{e} == {i}) x d[{i}];"); + } + } + let extract = |p: &mut String, into: &str| { + for (r, row) in checks.iter().enumerate() { + for &q in row { + let _ = writeln!(p, "cx d[{q}], a[{r}];"); + } + } + let _ = writeln!(p, "{into} = measure a;"); + }; + extract(&mut p, "s"); + let _ = writeln!(p, "corr = {f}(s);"); + for i in 0..n { + let _ = writeln!(p, "if (corr[{i}]) x d[{i}];"); + } + p.push_str("reset a;\n"); + extract(&mut p, "s2"); + p.push_str("fin = measure d;\n"); + p +} + +/// The loading and rearrangement of a `w × h` array of traps into the target `target`: +/// every trap loaded with probability ½ (a Hadamard and a measurement), the planner called +/// on the occupancy, its moves applied as flips, and the array read out. The callout is +/// `plan64` when its result fits 64 bits, `plan` otherwise. +pub fn atom_program(w: u32, h: u32) -> String { + let n = (w * h) as usize; + let r = 2 * n + 1; + let f = if r <= 64 { "plan64" } else { "plan" }; + let mut p = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + let _ = writeln!(p, "// A {w} x {h} array of traps, loaded at random and rearranged in the middle of the circuit."); + let _ = writeln!(p, "extern {f}(bit[{n}]) -> bit[{r}];"); + let _ = writeln!(p, "qubit[{n}] q;\nbit[{n}] occ;\nbit[{r}] plan;\nbit[{n}] fin;"); + p.push_str("h q;\nocc = measure q;\n"); + let _ = writeln!(p, "plan = {f}(occ);"); + let _ = writeln!(p, "for uint i in [0:{}] {{\n if (plan[i]) x q[i];\n if (plan[{n} + i]) x q[i];\n}}", n - 1); + p.push_str("fin = measure q;\n"); + p +} + +/// A bundle of the decoder module and a memory round of a code. +pub fn qec_bundle(decoder: &[u8], n: usize, checks: &[Vec], errors: usize, run: Run) -> Bundle { + let f = if n > 64 || checks.len() > 64 { "decode" } else { "decode64" }; + Bundle { + program: qec_program(n, checks, errors), + inputs: Default::default(), + modules: vec![Module { name: "qldpc-decoder".into(), bytes: decoder.to_vec(), priors: vec![("checks".into(), checks_prior(n, checks))] }], + callouts: vec![Callout { name: f.into(), module: 0, export: f.into() }], + run, + } +} + +/// A bundle of the planner module and a rearrangement of a `w × h` array into its centred +/// `tw × th` block. +pub fn atom_bundle(planner: &[u8], w: u32, h: u32, tw: u32, th: u32, run: Run) -> Bundle { + let f = if 2 * w * h < 64 { "plan64" } else { "plan" }; + Bundle { + program: atom_program(w, h), + inputs: Default::default(), + modules: vec![Module { name: "atom-planner".into(), bytes: planner.to_vec(), priors: vec![("target".into(), target_prior(w, h, &target_block(w, h, tw, th)))] }], + callouts: vec![Callout { name: f.into(), module: 0, export: f.into() }], + run, + } +} diff --git a/wai/wai-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs new file mode 100644 index 000000000..737818485 --- /dev/null +++ b/wai/wai-wasmq/src/run.rs @@ -0,0 +1,646 @@ +//! Running a bundle (extensions/quantum-wasmq.md §6): the program's steps shot by shot on +//! a stabilizer tableau, each `extern` call a metered call into its module, and the result +//! sealed in the job receipt the engine already signs, with the fuel, the modules' +//! digests, a digest of every call's inputs and outputs, and the fault that stopped the +//! run, if one did. Nothing here computes in floating point: a measurement's outcome is +//! determined or it is one bit of an integer stream. + +use crate::abi::{self, Fault, Stop}; +use crate::engine::{self, Instance}; +use crate::envelope::{Bundle, Opened, blake3_hex, hex}; +use crate::interp; +use crate::meter; +use crate::profile; +use serde_json::{Map, Value, json}; +use std::collections::BTreeMap; +use wai_quantum::ed25519_dalek::SigningKey; +use wai_quantum::qasm::{CalloutBit, Instr, Program, Step}; +use wai_quantum::quantum_tableau::{Clifford, Tableau}; + +/// The result's format, its members (§6.4, `fault` aside), what its `energy` says, the +/// members of a job body and the fault reasons (§6.2): the engine's, where every verifier +/// of a job reads them (`engine::wasmq`), so a run writes what a verifier holds it to. +pub use wai_quantum::engine::wasmq::{BODY, FORMAT, REASONS, RESULT, UNMETERED, body_status}; + +/// The engine a run uses. None of them appears in a receipt: they give the same bytes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EngineKind { + /// The pinned native engine on the prepared module. + Native, + /// The interpreter on the original module, metering it itself. + Interpreter, + /// The interpreter on the prepared module, which meters itself. + InterpreterPrepared, +} + +impl EngineKind { + pub fn parse(s: &str) -> Result { + match s { + "native" => Ok(EngineKind::Native), + "interpreter" => Ok(EngineKind::Interpreter), + "interpreter-prepared" => Ok(EngineKind::InterpreterPrepared), + _ => Err(format!("engine {s:?}: native, interpreter or interpreter-prepared")), + } + } + + /// The default here: the native engine where this build has it. + pub fn default_here() -> EngineKind { + if engine::available().contains(&"native") { EngineKind::Native } else { EngineKind::Interpreter } + } +} + +/// Why a run produced no receipt: the bundle was refused, or the host could not run it. +#[derive(Debug)] +pub enum Error { + Refused(profile::Refusal), + Host(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Refused(r) => write!(f, "{r}"), + Error::Host(h) => write!(f, "the host could not run the bundle: {h}"), + } + } +} + +/// One call, for a transcript a person asks for (`--transcript`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Call { + pub shot: u64, + pub callout: String, + pub input: Vec, + pub output: Vec, + pub fuel: u64, +} + +/// The seed of shot `k`: a step of SplitMix64 from the run's seed (§6.1). +pub fn shot_seed(seed: u64, shot: u64) -> u64 { + let mut z = seed.wrapping_add(shot.wrapping_add(1).wrapping_mul(0x9e37_79b9_7f4a_7c15)); + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) +} + +/// A shot's stream of outcomes (§6.1): SplitMix64 from the shot's seed XOR `0x51574d31`. +pub struct Outcomes(u64); + +impl Outcomes { + pub fn of_shot(seed: u64, shot: u64) -> Outcomes { + Outcomes(shot_seed(seed, shot) ^ 0x5157_4d31) + } + + /// The outcome of the next measurement whose outcome is random: 1 when the stream's + /// next value is below 2^63. + pub fn next_reads_one(&mut self) -> bool { + self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + (z ^ (z >> 31)) < (1 << 63) + } +} + +struct Budget { + per_call: u64, + job_left: u64, +} + +impl Budget { + /// The fuel a call may use: its own budget, or what is left of the run's when that + /// is less (§4.2). + fn limit(&self, per: u64) -> (u64, bool) { + if per <= self.job_left { (per, false) } else { (self.job_left, true) } + } +} + +/// A call that stopped, before it is written into the result. +struct Stopped { + fault: Fault, + phase: &'static str, + /// Whose budget ran out, for a fuel fault. + budget: &'static str, + fuel: u64, +} + +/// One metered stretch of guest work (§4.2): the fuel is set to the limit, `f` runs, and +/// what was used is taken from the run's budget. A stretch that runs out used its limit. +fn metered(inst: &mut dyn Instance, b: &mut Budget, per: u64, own: &'static str, f: impl FnOnce(&mut dyn Instance) -> Result) -> (Result, u64, &'static str) { + let (limit, run) = b.limit(per); + inst.set_fuel(limit as i64); + let r = f(inst); + let left = inst.fuel(); + let used = if left < 0 { limit } else { limit - left as u64 }; + b.job_left -= used; + (r, used, if run { "run" } else { own }) +} + +/// The prepared modules of a bundle, for an engine that runs them: none for the +/// interpreter, which meters a module as written, so that a host keeps no copy it does not use. +fn prepared_for(b: &Bundle, kind: EngineKind) -> Result>, Error> { + if kind == EngineKind::Interpreter { + return Ok(Vec::new()); + } + b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused) +} + +/// Runs `f` where the engine's calls can run: on the native engine, one thread with the +/// stack a call needs for the whole run (`engine::on_its_stack`). +fn on_stack(kind: EngineKind, f: impl FnOnce() -> Result + Send) -> Result { + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + if kind == EngineKind::Native { + return engine::on_its_stack(f).map_err(Error::Host)?; + } + let _ = kind; + f() +} + +/// The engines of a bundle's modules, in order. +fn engines<'a>(b: &'a Bundle, prepared: &'a [Vec], kind: EngineKind) -> Result>, Error> { + let mut out: Vec> = Vec::new(); + for (k, m) in b.modules.iter().enumerate() { + let p = || prepared.get(k).map(Vec::as_slice).ok_or_else(|| Error::Host("a prepared module missing".into())); + out.push(match kind { + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + EngineKind::Native => Box::new(engine::Native::new(p()?).map_err(Error::Host)?), + #[cfg(not(all(feature = "wasmtime", not(target_arch = "wasm32"))))] + EngineKind::Native => return Err(Error::Host("this build has no native engine".into())), + EngineKind::Interpreter => Box::new(engine::Interpreter::new(&m.bytes, interp::Metering::Own).map_err(Error::Host)?), + EngineKind::InterpreterPrepared => Box::new(engine::Interpreter::new(p()?, interp::Metering::Module).map_err(Error::Host)?), + }); + } + Ok(out) +} + +/// A fresh instance of module `k` (§5.3): its seed, its priors and its `init`, each +/// metered under the instance budget. The fuel the calls used is added to `used`. +fn instance<'e>(b: &Bundle, k: usize, facts: &profile::Facts, engine: &'e dyn engine::Engine, shot: u64, budget: &mut Budget, used: &mut u64) -> Result, Stopped>, Error> { + let mut i = engine.instantiate().map_err(Error::Host)?; + let seed = if b.run.state == "shot" { shot_seed(b.run.seed, shot) } else { b.run.seed }; + let m = &b.modules[k]; + let mut phases: Vec<(&'static str, Option)> = Vec::new(); + if facts.functions.contains_key("wasmq-seed") { + phases.push(("seed", None)); + } + phases.extend((0..m.priors.len()).map(|j| ("prior", Some(j)))); + if facts.functions.contains_key("init") { + phases.push(("init", None)); + } + for (phase, prior) in phases { + let (r, n, budget_name) = metered(i.as_mut(), budget, b.run.per_init, "instance", |i| match (phase, prior) { + ("seed", _) => abi::lifecycle(i, "wasmq-seed", &[seed]), + (_, Some(j)) => abi::prior(i, j as u32, &m.priors[j].1), + _ => abi::lifecycle(i, "init", &[]), + }); + *used += n; + match r { + Ok(()) => {} + Err(Stop::Host(h)) => return Err(Error::Host(h)), + Err(Stop::Fault(fault)) => return Ok(Err(Stopped { fault, phase, budget: budget_name, fuel: n })), + } + } + Ok(Ok(i)) +} + +/// Runs an opened bundle on `kind`, sealing the result with `key`: the job body, or why +/// there is none. `log` asks for every call's inputs and outputs as well. +pub fn run(opened: &Opened, kind: EngineKind, key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { + if !log { + return Ok((run_recording(opened, kind, key, None)?, None)); + } + let mut record = Vec::new(); + let body = run_recording(opened, kind, key, Some(&mut |c: &Call| record.push(c.clone())))?; + Ok((body, Some(record))) +} + +/// [`run`], giving each call that returns to `on_call` as it returns, so that a record of +/// a run of any length is written as it is made, and never held whole. +pub fn run_recording(opened: &Opened, kind: EngineKind, key: &SigningKey, on_call: Option<&mut (dyn FnMut(&Call) + Send)>) -> Result { + on_stack(kind, || { + let prepared = prepared_for(&opened.bundle, kind)?; + let engines = engines(&opened.bundle, &prepared, kind)?; + execute(opened, &engines, key, on_call) + }) +} + +/// A transcript's digest, begun (§6.4): the prefix every verifier of a job's form reads too +/// (rule 35). +fn transcript_start() -> blake3::Hasher { + let mut t = blake3::Hasher::new(); + t.update(wai_quantum::engine::wasmq::TRANSCRIPT_PREFIX); + t +} + +/// One returned call in the transcript's digest (§6.4). +fn hash_call(t: &mut blake3::Hasher, shot: u64, func: u32, input: &[u8], output: &[u8], fuel: u64) { + for part in [&shot.to_le_bytes()[..], &func.to_le_bytes(), &(input.len() as u32).to_le_bytes(), input, &(output.len() as u32).to_le_bytes(), output, &fuel.to_le_bytes()] { + t.update(part); + } +} + +/// The digest of a record of calls, as a receipt seals it for a run that ended `ok`, or +/// nothing when a call names no `extern` of the program. [`replay`] checks each call; this +/// is what shows that a record is the whole of a run's, none left out or added. +pub fn transcript_blake3(opened: &Opened, calls: &[Call]) -> Option { + let mut t = transcript_start(); + for c in calls { + let func = opened.bundle.callouts.iter().position(|x| x.name == c.callout)?; + hash_call(&mut t, c.shot, func as u32, &c.input, &c.output, c.fuel); + } + Some(hex(t.finalize().as_bytes())) +} + +/// The width of the bits a program's steps read and write. +pub(crate) fn width(p: &Program, steps: &[Step]) -> usize { + let mut w = p.n_bits as usize; + for s in steps { + match s { + Step::Instr(Instr::Measure { bit, .. }) => w = w.max(*bit as usize + 1), + Step::Instr(Instr::JumpUnless(c, _)) => w = w.max(c.bits.iter().map(|&b| b as usize + 1).max().unwrap_or(0)), + Step::Callout { input, output, .. } => { + for b in input { + if let CalloutBit::Bit(b) = b { + w = w.max(*b as usize + 1); + } + } + w = w.max(output.iter().map(|&b| b as usize + 1).max().unwrap_or(0)); + } + _ => {} + } + } + w +} + +fn execute<'e>(opened: &Opened, engines: &'e [Box], key: &SigningKey, mut on_call: Option<&mut (dyn FnMut(&Call) + Send)>) -> Result { + let b: &Bundle = &opened.bundle; + let (h, method) = crate::envelope::read(&b.program, &b.inputs).map_err(Error::Refused)?; + let (p, steps, externs) = (&h.reading.program, &h.steps, &h.externs); + let w = width(p, steps); + let facts: Vec = b.modules.iter().map(|m| profile::check(&m.bytes)).collect::>().map_err(Error::Refused)?; + let gates: Vec> = p.clifford_ops().map_err(|e| Error::Refused(profile::Refusal::new("W016", e)))?.into_iter().map(|(g, _)| g).collect(); + let host = |e: wai_quantum::quantum_tableau::TableauError| Error::Host(e.to_string()); + + let mut live: Vec>> = engines.iter().map(|_| None).collect(); + let mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; + let (mut used, mut init, mut calls) = (0u64, 0u64, 0u64); + let mut by_callout: BTreeMap = BTreeMap::new(); + let mut transcript = transcript_start(); + let mut counts: BTreeMap = BTreeMap::new(); + let mut shots_run = 0u64; + // The fault that stopped the run: where, and what. + let mut stopped: Option<(Stopped, u64, String, Option)> = None; + + 'shots: for shot in 0..b.run.shots { + // Instances: fresh for each shot, or once for the run. + if b.run.state == "shot" || shot == 0 { + for k in 0..b.modules.len() { + let mut n = 0; + // The last shot's goes first: a host holds one instance of a module at a time. + live[k] = None; + let made = instance(b, k, &facts[k], engines[k].as_ref(), shot, &mut budget, &mut n)?; + used += n; + init += n; + match made { + Ok(i) => live[k] = Some(i), + Err(s) => { + stopped = Some((s, shot, b.modules[k].name.clone(), None)); + break 'shots; + } + } + } + } + let mut outcomes = Outcomes::of_shot(b.run.seed, shot); + let mut bits = vec![false; w]; + let mut t = Tableau::new(p.n_qubits).map_err(host)?; + let mut pc = 0; + while pc < steps.len() { + match &steps[pc] { + Step::Instr(Instr::Gate(i)) => { + for &c in &gates[*i] { + t.apply(c).map_err(host)?; + } + } + Step::Instr(Instr::Measure { qubit, bit }) => { + bits[*bit as usize] = t.measure(*qubit, || outcomes.next_reads_one()).map_err(host)?.0; + } + Step::Instr(Instr::Reset(qubit)) => { + if t.measure(*qubit, || outcomes.next_reads_one()).map_err(host)?.0 { + t.apply(Clifford::X(*qubit)).map_err(host)?; + } + } + Step::Instr(Instr::JumpUnless(c, to)) => { + if !c.holds(&bits) { + pc = *to; + continue; + } + } + Step::Instr(Instr::Jump(to)) => { + pc = *to; + continue; + } + Step::Callout { func, input, output } => { + let decl = &externs[*func as usize]; + let c = &b.callouts[*func as usize]; + let ins: Vec = input + .iter() + .map(|x| match x { + CalloutBit::Bit(i) => bits[*i as usize], + CalloutBit::Const(v) => *v, + }) + .collect(); + let has_post = facts[c.module].functions.contains_key(&format!("cabi_post_{}", c.export)); + let i = live[c.module].as_mut().ok_or_else(|| Error::Host("an instance missing".into()))?; + let per = budget.per_call; + let (r, n, budget_name) = metered(i.as_mut(), &mut budget, per, "call", |i| abi::call(i, decl, &c.export, &ins, has_post)); + used += n; + calls += 1; + let e = by_callout.entry(decl.name.clone()).or_default(); + e.0 += 1; + e.1 += n; + e.2 = e.2.max(n); + let in_bytes = abi::pack(&ins); + match r { + Ok(outs) => { + let out_bytes = abi::pack(&outs); + for (k, &bit) in output.iter().enumerate() { + bits[bit as usize] = outs[k]; + } + hash_call(&mut transcript, shot, *func, &in_bytes, &out_bytes, n); + if let Some(f) = on_call.as_mut() { + f(&Call { shot, callout: decl.name.clone(), input: in_bytes, output: out_bytes, fuel: n }); + } + } + Err(Stop::Host(h)) => return Err(Error::Host(h)), + Err(Stop::Fault(fault)) => { + stopped = Some((Stopped { fault, phase: "call", budget: budget_name, fuel: n }, shot, b.modules[c.module].name.clone(), Some(decl.name.clone()))); + break 'shots; + } + } + } + } + pc += 1; + } + let key: String = (0..p.n_bits as usize).rev().map(|i| if bits[i] { '1' } else { '0' }).collect(); + *counts.entry(key).or_default() += 1; + shots_run += 1; + } + if let Some((s, shot, ..)) = &stopped { + for part in [&b"fault"[..], &shot.to_le_bytes(), s.fault.reason.as_bytes(), &s.fuel.to_le_bytes()] { + transcript.update(part); + } + } + + let mut functions = Map::new(); + for (name, (calls, fuel, max)) in &by_callout { + functions.insert(name.clone(), json!({ "calls": calls, "fuel": fuel, "max": max })); + } + let mut result = json!({ + "format": FORMAT, + "profile": profile::PROFILE, + "status": if stopped.is_some() { "fault" } else { "ok" }, + "method": method, + "qubits": p.n_qubits, + "shots": b.run.shots, + "shots_run": shots_run, + "seed": b.run.seed, + "counts": counts, + "callouts": calls, + "fuel": { + "used": used, + "instances": init, + "budget": { "per_call": b.run.per_call, "per_job": b.run.per_job, "per_instance": b.run.per_init }, + "callouts": functions, + }, + "modules": b.modules.iter().map(|m| json!({ "name": m.name, "blake3": blake3_hex(&m.bytes) })).collect::>(), + "transcript_blake3": hex(transcript.finalize().as_bytes()), + "accuracy": { "class": "exact" }, + }); + if let Some((s, shot, module, callout)) = stopped { + let mut f = json!({ "reason": s.fault.reason, "shot": shot, "phase": s.phase, "module": module, "fuel": s.fuel }); + if let Some(c) = callout { + f["callout"] = json!(c); + } + if s.fault.reason == "fuel_exhausted" { + f["budget"] = json!(s.budget); + } + result["fault"] = f; + } + Ok(seal(opened, result, key)) +} + +/// Replays recorded calls against a bundle's modules (extensions/quantum-wasmq.md §7): each +/// call's input, in order, through a fresh instance for each shot (or one for the run, as +/// the contract's `state` says), seeded and given its priors as a run would; the calls +/// whose output or fuel differ from the record, as messages. The quantum part is not run: +/// the inputs are as given, from a run here or from a machine that ran the module. +pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result, Error> { + replay_from(opened, &mut calls.iter().cloned(), kind).map(|r| r.problems) +} + +/// What [`replay_from`] found. +#[derive(Debug)] +pub struct Replayed { + /// The calls read. + pub calls: u64, + /// The first [`PROBLEMS_KEPT`] calls that differ, as messages. + pub problems: Vec, + /// How many more differ. + pub more_problems: u64, + /// The record's digest, as a receipt of the run that made it seals it when the run ended + /// `ok` (§6.4); none when a call names no `extern` of the program. Each call shows only + /// itself: this is what shows a record is the whole of a run's. + pub transcript_blake3: Option, +} + +/// The most messages [`replay_from`] keeps. +pub const PROBLEMS_KEPT: usize = 1000; + +/// [`replay`], reading the calls as they come, so that a record of any length is never +/// held whole. +pub fn replay_from(opened: &Opened, calls: &mut (dyn Iterator + Send), kind: EngineKind) -> Result { + on_stack(kind, || replay_here(opened, calls, kind)) +} + +fn replay_here(opened: &Opened, calls: &mut (dyn Iterator + Send), kind: EngineKind) -> Result { + let b = &opened.bundle; + let (h, _) = crate::envelope::read(&b.program, &b.inputs).map_err(Error::Refused)?; + let externs = &h.externs; + let prepared = prepared_for(b, kind)?; + let facts: Vec = b.modules.iter().map(|m| profile::check(&m.bytes)).collect::>().map_err(Error::Refused)?; + let engines = engines(b, &prepared, kind)?; + let mut live: Vec)>> = engines.iter().map(|_| None).collect(); + let mut out = Replayed { calls: 0, problems: Vec::new(), more_problems: 0, transcript_blake3: None }; + let mut transcript = Some(transcript_start()); + let problem = |out: &mut Replayed, p: String| { + if out.problems.len() < PROBLEMS_KEPT { + out.problems.push(p); + } else { + out.more_problems += 1; + } + }; + let mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; + let mut last = 0; + for (k, call) in calls.enumerate() { + out.calls += 1; + match (externs.iter().position(|d| d.name == call.callout), transcript.as_mut()) { + (Some(func), Some(t)) => hash_call(t, call.shot, func as u32, &call.input, &call.output, call.fuel), + _ => transcript = None, + } + if call.shot >= b.run.shots { + problem(&mut out, format!("call {k}: shot {}, where the run has {} shots", call.shot, b.run.shots)); + continue; + } + if call.shot < last { + problem(&mut out, format!("call {k}: shot {} after shot {last}: a run's calls come in the order of its shots", call.shot)); + continue; + } + last = call.shot; + let Some(func) = externs.iter().position(|d| d.name == call.callout) else { + problem(&mut out, format!("call {k}: the program declares no `extern {}`", call.callout)); + continue; + }; + let decl = &externs[func]; + let c = &b.callouts[func]; + let fresh = match &live[c.module] { + None => true, + Some((shot, _)) => b.run.state == "shot" && *shot != call.shot, + }; + if fresh { + live[c.module] = None; + match instance(b, c.module, &facts[c.module], engines[c.module].as_ref(), call.shot, &mut budget, &mut 0)? { + Ok(i) => live[c.module] = Some((call.shot, i)), + Err(s) => { + problem(&mut out, format!("call {k} ({} in shot {}): the module stops with {} in its {}", call.callout, call.shot, s.fault.reason, s.phase)); + break; + } + } + } + let width: u32 = decl.args.iter().map(|a| a.bits()).sum(); + if call.input.len() != (width as usize).div_ceil(8) { + problem(&mut out, format!("call {k} ({}): {} input bytes, where its arguments take {}", call.callout, call.input.len(), (width as usize).div_ceil(8))); + continue; + } + let ins: Vec = (0..width as usize).map(|i| call.input[i / 8] >> (i % 8) & 1 == 1).collect(); + let has_post = facts[c.module].functions.contains_key(&format!("cabi_post_{}", c.export)); + let i = &mut live[c.module].as_mut().ok_or_else(|| Error::Host("an instance missing".into()))?.1; + let per = budget.per_call; + let (r, used, _) = metered(i.as_mut(), &mut budget, per, "call", |i| abi::call(i, decl, &c.export, &ins, has_post)); + match r { + Ok(outs) => { + let got = abi::pack(&outs); + if got != call.output { + problem(&mut out, format!("call {k} ({} in shot {}): the module returns {}, the record says {}", call.callout, call.shot, hex(&got), hex(&call.output))); + } + if used != call.fuel { + problem(&mut out, format!("call {k} ({} in shot {}): the module uses {used} fuel, the record says {}", call.callout, call.shot, call.fuel)); + } + } + Err(Stop::Fault(f)) => problem(&mut out, format!("call {k} ({} in shot {}): the module stops with {}", call.callout, call.shot, f.reason)), + Err(Stop::Host(h)) => return Err(Error::Host(h)), + } + } + out.transcript_blake3 = transcript.map(|t| hex(t.finalize().as_bytes())); + Ok(out) +} + +/// The request a receipt binds: the contract and the envelope's digest. +pub fn request(opened: &Opened) -> Value { + json!({ "wasmq": opened.contract, "envelope_blake3": opened.envelope_blake3 }) +} + +/// The job body: the request, the result with its energy and its receipt (the engine's +/// job receipt, `engine::receipt`). +pub fn seal(opened: &Opened, mut result: Value, key: &SigningKey) -> Value { + result["energy"] = json!({ "unmetered": UNMETERED }); + let req = request(opened); + let receipt = wai_quantum::engine::receipt::seal(key, &req, &result); + let status = body_status(&result); + result["receipt"] = receipt; + json!({ "status": status, "method": result["method"], "request": req, "result": result }) +} + +/// What [`verify`] found. +#[derive(Debug, Default)] +pub struct Verdict { + /// Whether the receipt is a valid seal of the request and the result under the key + /// it names, by the engine's own check (`engine::job::verify`). + pub signature: bool, + /// The key the receipt names, as 64 hexadecimal digits: who signed. A receipt shows + /// that this key sealed this result; whether it is a key the reader trusts is the + /// reader's to compare. + pub signer: Option, + /// How the run ended, from the result the signature covers: `ok` or `fault`. + pub outcome: Option, + /// What makes the job other than one this profile writes; empty when it is one. Only + /// what is sealed decides it. + pub well_formed: Vec, + /// Where the host's unsealed words in the body (its `status`, its `method`, members + /// nothing seals) differ from what is sealed: told to a reader, beside the verdict, + /// and no part of it. + pub notes: Vec, + /// With the bundle: whether it is the one the receipt binds, and whether running it + /// again gives the same result, byte for byte. + pub bundle: Option, + pub reproduced: Option, +} + +impl Verdict { + pub fn ok(&self) -> bool { + self.signature && self.well_formed.is_empty() && self.bundle != Some(false) && self.reproduced != Some(false) + } +} + +/// Checks a wasmQ job body as every job is checked (`engine::job::check`, which holds a +/// wasmQ job to its profile's form, §6.5): its signature and who signed; that its request +/// and result are ones this profile writes, by what is sealed; and, given the bundle, that it +/// is the bundle the receipt binds and that running it again reproduces the result. Where +/// the body's unsealed members differ from what is sealed, the verdict notes it and does +/// not change. +pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result { + use wai_quantum::engine::job; + use wai_quantum::engine::receipt::{BAD_KEY, BAD_SIGNATURE, DIGEST_MISMATCH, RECEIPT_FORM}; + let (Some(_), Some(r)) = (job.get("request"), job.get("result")) else { + return Err("expected a job: {\"request\": ..., \"result\": ...}, the result carrying its receipt".into()); + }; + let seal = r.get("receipt").ok_or("the result carries no receipt")?; + let c = job::check(job); + let sealing = [RECEIPT_FORM, DIGEST_MISMATCH, BAD_KEY, BAD_SIGNATURE]; + let w = c.wasmq.clone().unwrap_or(Value::Null); + let said = |k: &str| w[k].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect::>(); + // Every other reason of the provider protocol's §8.4, then each rule of §6.5 the job breaks. + let mut well_formed: Vec = c.reasons.iter().filter(|x| !sealing.contains(x) && **x != job::WASMQ_FORM).map(|x| format!("the provider protocol's reason {x}")).collect(); + well_formed.extend(said("problems")); + if c.wasmq.is_none() { + well_formed.push(format!("the result's format is {}, not {FORMAT}", r["format"])); + } + let c_ = &job["request"]["wasmq"]; + let mut v = Verdict { + signature: !c.reasons.iter().any(|x| sealing.contains(x)), + signer: seal["key"].as_str().map(str::to_string), + outcome: w["outcome"].as_str().map(str::to_string), + well_formed, + notes: said("notes"), + ..Verdict::default() + }; + if let Some(o) = bundle { + // The contract as it is sealed, in its canonical form: a number written another way + // (4.0 for 4) is the same contract, as it is the same receipt. + let canonical = wai_quantum::engine::receipt::canonical; + let same = o.envelope_blake3 == job["request"]["envelope_blake3"] && canonical(&o.contract) == canonical(c_); + v.bundle = Some(same); + if same { + let k = SigningKey::from_bytes(&[7; 32]); + let (again, _) = run(o, kind, &k, false).map_err(|e| e.to_string())?; + let strip = |x: &Value| { + let mut x = x.clone(); + if let Some(o) = x.as_object_mut() { + o.remove("receipt"); + } + wai_quantum::engine::receipt::canonical(&x) + }; + v.reproduced = Some(strip(&again["result"]) == strip(r)); + } + } + Ok(v) +} diff --git a/wai/wai-wasmq/src/vectors.rs b/wai/wai-wasmq/src/vectors.rs new file mode 100644 index 000000000..9a1823d4f --- /dev/null +++ b/wai/wai-wasmq/src/vectors.rs @@ -0,0 +1,86 @@ +//! The conformance vectors (`wai/quantum-conformance/wasmq/`, extensions/quantum-wasmq.md +//! §9): envelopes with the results they must give on every engine and every host, and +//! modules and envelopes a conforming runner refuses, each with its code. + +use crate::envelope::{blake3_hex, open}; +use crate::profile; +use crate::run::{EngineKind, run}; +use serde_json::{Value, json}; +use wai_quantum::ed25519_dalek::SigningKey; +use wai_quantum::engine::receipt::canonical; + +/// The key the vectors' receipts are sealed with. A receipt's digest does not depend on +/// the key; its signature does, and a vector pins both. +pub fn key() -> SigningKey { + SigningKey::from_bytes(&[7; 32]) +} + +/// What a vector pins of a run: the job body's parts that every engine on every host +/// must give exactly. +pub fn pins(body: &Value) -> Value { + let r = &body["result"]; + let mut bare = r.clone(); + if let Some(o) = bare.as_object_mut() { + o.remove("receipt"); + } + json!({ + "status": r["status"], + "fault": r.get("fault").cloned().unwrap_or(Value::Null), + "fuel_used": r["fuel"]["used"], + "callouts": r["callouts"], + "transcript_blake3": r["transcript_blake3"], + "result_blake3": blake3_hex(&canonical(&bare)), + "receipt_digest": r["receipt"]["digest"], + "job_blake3": blake3_hex(&canonical(body)), + }) +} + +/// Checks every vector under `dir` on `kinds`: one line a vector, and the problems. +pub fn verify(dir: &std::path::Path, kinds: &[EngineKind]) -> Result<(Vec, Vec), String> { + let read = |p: &str| std::fs::read(dir.join(p)).map_err(|e| format!("{p}: {e}")); + let index: Value = serde_json::from_slice(&read("vectors.json")?).map_err(|e| format!("vectors.json: {e}"))?; + let (mut lines, mut problems) = (Vec::new(), Vec::new()); + for v in index["runs"].as_array().ok_or("vectors.json has no runs")? { + let file = v["file"].as_str().unwrap_or(""); + let bytes = read(file)?; + if blake3_hex(&bytes) != v["envelope_blake3"] { + problems.push(format!("{file}: the envelope is not the one pinned")); + continue; + } + let o = match open(&bytes) { + Ok(o) => o, + Err(e) => { + problems.push(format!("{file}: refused: {e}")); + continue; + } + }; + // The line carries what this host computed, so two hosts' digests compare their + // outputs, not only the pins they both read. + let mut computed = Vec::new(); + for &k in kinds { + match run(&o, k, &key(), false) { + Ok((body, _)) => { + let got = pins(&body); + if got != v["expect"] { + problems.push(format!("{file} on {k:?}: {got} where the vector pins {}", v["expect"])); + } + computed.push(format!("{}:{}", got["status"].as_str().unwrap_or(""), got["job_blake3"].as_str().unwrap_or(""))); + } + Err(e) => problems.push(format!("{file} on {k:?}: {e}")), + } + } + computed.dedup(); + lines.push(format!("{file} {}", computed.join(" "))); + } + for v in index["refuse"].as_array().ok_or("vectors.json has no refusals")? { + let file = v["file"].as_str().unwrap_or(""); + let bytes = read(file)?; + let got = if file.ends_with(".wai") { open(&bytes).err().map(|r| r.code) } else { profile::check(&bytes).err().map(|r| r.code) }; + if got != v["code"].as_str() { + problems.push(format!("{file}: refused as {got:?}, where the vector pins {}", v["code"])); + } + // What this host computed, as for a run: two hosts' digests then compare their refusals. + lines.push(format!("{file} refused {}", got.unwrap_or("nothing: accepted"))); + } + Ok((lines, problems)) +} diff --git a/wai/wai-wasmq/tests/adversarial.rs b/wai/wai-wasmq/tests/adversarial.rs new file mode 100644 index 000000000..68b2d241c --- /dev/null +++ b/wai/wai-wasmq/tests/adversarial.rs @@ -0,0 +1,328 @@ +//! What must fail does (extensions/quantum-wasmq.md §3, §4, §6): a module that could +//! reach outside itself or let a host choose its result is refused before it runs; a +//! bundle whose bytes are not the ones its contract pins is refused; a callout that runs +//! past its fuel, its call depth or its result's width ends the run in a signed fault +//! receipt, the same on every engine; and each protection is shown to be load-bearing by +//! the failure that appears when it is taken away. + +use serde_json::Value; +use wai_quantum::ed25519_dalek::SigningKey; +use wai_wasmq::engine::{Engine, Interpreter, Trap}; +use wai_wasmq::envelope::{Bundle, Callout, Module, Run, open, pack}; +use wai_wasmq::interp::Metering; +use wai_wasmq::meter::{self, Parts}; +use wai_wasmq::run::{EngineKind, run}; + +fn wasm(wat: &str) -> Vec { + wat::parse_str(wat).unwrap() +} + +/// A program that measures eight qubits into `b` and calls `f` on them. +const PROGRAM: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nh q;\nb = measure q;\nr = f(b);\n"; + +fn bundle(module: Vec, run: Run) -> Bundle { + Bundle { program: PROGRAM.into(), inputs: Default::default(), modules: vec![Module { name: "m".into(), bytes: module, priors: vec![] }], callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], run } +} + +fn refused(module: &str) -> &'static str { + match pack(&bundle(wasm(module), Run::new(4, 1))) { + Ok(_) => "packed", + Err(r) => r.code, + } +} + +fn kinds() -> Vec { + #[allow(unused_mut)] // the native engine is added where this build has it + let mut k = vec![EngineKind::Interpreter, EngineKind::InterpreterPrepared]; + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + k.insert(0, EngineKind::Native); + k +} + +/// Runs a bundle on every engine: the job bodies are byte-identical; the first. +fn everywhere(b: &Bundle) -> Value { + let o = open(&pack(b).unwrap()).unwrap(); + let key = SigningKey::from_bytes(&[7; 32]); + let mut first: Option> = None; + let mut body = Value::Null; + for k in kinds() { + let (v, _) = run(&o, k, &key, false).unwrap(); + let bytes = wai_quantum::engine::receipt::canonical(&v); + match &first { + None => first = Some(bytes), + Some(f) => assert_eq!(f, &bytes, "{k:?} gives other bytes"), + } + body = v; + } + let verdict = wai_wasmq::verify(&body, Some(&o), kinds()[0]).unwrap(); + assert!(verdict.ok(), "{verdict:?}"); + body +} + +const IDENTITY: &str = r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) local.get 0))"#; + +#[test] +fn the_positive_control_packs_and_runs() { + assert_eq!(refused(IDENTITY), "packed"); + let body = everywhere(&bundle(wasm(IDENTITY), Run::new(16, 3))); + assert_eq!(body["result"]["status"], "ok"); + // r = f(b) = b in every shot: the high byte of each key equals the low byte. + for (k, _) in body["result"]["counts"].as_object().unwrap() { + assert_eq!(&k[..8], &k[8..], "{k}"); + } +} + +#[test] +fn a_module_that_imports_a_clock_is_refused() { + let clock = r#"(module (import "wasi:clocks/monotonic-clock" "now" (func $now (result i64))) + (func (export "f") (param i64) (result i64) call $now))"#; + assert_eq!(refused(clock), "W002"); + let random = r#"(module (import "wasi_snapshot_preview1" "random_get" (func (param i32 i32) (result i32))) (func (export "f") (param i64) (result i64) local.get 0))"#; + assert_eq!(refused(random), "W002"); + let memory = r#"(module (import "env" "memory" (memory 1 1)) (func (export "f") (param i64) (result i64) local.get 0))"#; + assert_eq!(refused(memory), "W002"); +} + +#[test] +fn a_memory_without_a_maximum_or_past_the_profiles_is_refused() { + assert_eq!(refused(r#"(module (memory 1) (func (export "f") (param i64) (result i64) local.get 0))"#), "W003"); + assert_eq!(refused(r#"(module (memory 1 65536) (func (export "f") (param i64) (result i64) local.get 0))"#), "W003"); + assert_eq!(refused(r#"(module (memory 1 1 shared) (func (export "f") (param i64) (result i64) local.get 0))"#), "W005"); +} + +#[test] +fn threads_vectors_and_a_start_function_are_refused() { + assert_eq!(refused(r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) i32.const 0 local.get 0 i64.atomic.rmw.add))"#), "W005"); + assert_eq!(refused(r#"(module (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat i64x2.extract_lane 0))"#), "W006"); + assert_eq!(refused(r#"(module (func $s) (start $s) (func (export "f") (param i64) (result i64) local.get 0))"#), "W004"); +} + +#[test] +fn growing_memory_past_the_maximum_fails_the_same_way_everywhere() { + // Grows by the low bits of its input: past the maximum of 3 pages, memory.grow gives -1. + let m = wasm(r#"(module (memory 1 3) (func (export "f") (param i64) (result i64) + local.get 0 i32.wrap_i64 i32.const 7 i32.and memory.grow i64.extend_i32_s i64.const 255 i64.and))"#); + let body = everywhere(&bundle(m, Run::new(32, 5))); + let counts = body["result"]["counts"].as_object().unwrap(); + let mut saw_fail = false; + for k in counts.keys() { + let b = u8::from_str_radix(&k[8..], 2).unwrap() & 7; + let r = u8::from_str_radix(&k[..8], 2).unwrap(); + if b <= 2 { + assert_eq!(r, 1, "{k}: grew from one page"); + } else { + assert_eq!(r, 255, "{k}: past the maximum"); + saw_fail = true; + } + } + assert!(saw_fail); +} + +/// Returns the low 8 bits of a NaN's payload made by arithmetic on a NaN with a payload. +const NAN_PATH: &str = r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) + f64.const nan:0x4000000000abc local.get 0 f64.convert_i64_u f64.add i64.reinterpret_f64 i64.const 255 i64.and))"#; + +#[test] +fn a_nan_path_reads_the_canonical_nan_everywhere() { + let body = everywhere(&bundle(wasm(NAN_PATH), Run::new(16, 2))); + for k in body["result"]["counts"].as_object().unwrap().keys() { + assert_eq!(&k[..8], "00000000", "the canonical NaN's payload is zero: {k}"); + } +} + +#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] +#[test] +fn without_canonicalisation_a_nan_path_reads_what_the_host_chose() { + // The payload propagates through the addition on this host's processor (arm64 and + // x86_64 alike), so with neither the preparation's canonicalisation nor the engine's + // the module returns the payload's low byte, 0xbc; with either, zero. + let m = wasm(NAN_PATH); + let call = |parts: Parts, engine_nan: bool| -> u64 { + let p = meter::prepare_with(&m, parts).unwrap(); + let e = wai_wasmq::engine::Native::with(&p, engine_nan).unwrap(); + let mut i = e.instantiate().unwrap(); + i.set_fuel(1000); + i.call("f", &[3]).unwrap()[0] + }; + let off = Parts { nan: false, ..Parts::ALL }; + assert_eq!(call(off, false), 0xbc, "this test can fail: without canonicalisation the host's NaN shows"); + assert_eq!(call(Parts::ALL, false), 0, "the preparation alone canonicalises"); + assert_eq!(call(off, true), 0, "the engine alone canonicalises"); + assert_eq!(call(Parts::ALL, true), 0); +} + +#[test] +fn a_callout_past_its_fuel_ends_in_a_signed_fault_receipt() { + // Spins for as many iterations as its input says. + let m = wasm(r#"(module (func (export "f") (param i64) (result i64) (local i64) + block loop local.get 0 i64.eqz br_if 1 local.get 0 i64.const 1 i64.sub local.set 0 br 0 end end local.get 1))"#); + let mut r = Run::new(64, 9); + r.per_call = 400; + let body = everywhere(&bundle(m.clone(), r.clone())); + assert_eq!(body["status"], "fault"); + let f = &body["result"]["fault"]; + assert_eq!(f["reason"], "fuel_exhausted"); + assert_eq!(f["budget"], "call"); + assert_eq!(f["fuel"], 400); + assert!(body["result"]["shots_run"].as_u64().unwrap() < 64); + // The run's budget, not the call's, when it is the smaller. + r.per_call = 1_000_000; + r.per_job = 3000; + let body = everywhere(&bundle(m, r)); + assert_eq!(body["result"]["fault"]["budget"], "run"); + assert!(body["result"]["fuel"]["used"].as_u64().unwrap() <= 3000); +} + +#[test] +fn an_endless_loop_stops_at_its_budget() { + let m = wasm(r#"(module (func (export "f") (param i64) (result i64) loop br 0 end i64.const 0))"#); + let mut r = Run::new(4, 1); + r.per_call = 10_000; + let body = everywhere(&bundle(m, r)); + assert_eq!(body["result"]["fault"]["reason"], "fuel_exhausted"); + assert_eq!(body["result"]["fault"]["shot"], 0); +} + +#[test] +fn recursion_past_the_stack_ends_in_a_fault_receipt() { + let m = wasm(r#"(module (func $r (param i64) (result i64) local.get 0 i64.const 1 i64.add call $r) + (func (export "f") (param i64) (result i64) local.get 0 call $r))"#); + let body = everywhere(&bundle(m, Run::new(2, 1))); + assert_eq!(body["result"]["fault"]["reason"], "stack_exhausted"); +} + +#[test] +fn a_trap_and_a_result_past_its_width_end_in_fault_receipts() { + let trap = wasm(r#"(module (func (export "f") (param i64) (result i64) local.get 0 i64.const 0 i64.div_u))"#); + assert_eq!(everywhere(&bundle(trap, Run::new(2, 1)))["result"]["fault"]["reason"], "integer_divide_by_zero"); + let wide = wasm(r#"(module (func (export "f") (param i64) (result i64) i64.const 256))"#); + let body = everywhere(&bundle(wide, Run::new(2, 1))); + assert_eq!(body["result"]["fault"]["reason"], "bad_result"); +} + +#[test] +fn a_callout_with_the_wrong_signature_is_refused() { + assert_eq!(refused(r#"(module (func (export "f") (param i32) (result i32) local.get 0))"#), "W015"); + assert_eq!(refused(r#"(module (func (export "g") (param i64) (result i64) local.get 0))"#), "W015"); +} + +fn envelope() -> Vec { + let mut b = bundle(wasm(IDENTITY), Run::new(4, 1)); + b.modules[0].priors.push(("table".into(), b"a prior's bytes".to_vec())); + // A module with priors exports wasmq-prior. + b.modules[0].bytes = wasm(r#"(module (memory 1 1) (export "memory" (memory 0)) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "wasmq-prior") (param i32 i32 i32)) + (func (export "f") (param i64) (result i64) local.get 0))"#); + pack(&b).unwrap() +} + +fn tamper(env: &[u8], find: &[u8], with: &[u8]) -> Vec { + let at = env.windows(find.len()).position(|w| w == find).expect("the bytes are in the envelope"); + let mut e = env.to_vec(); + e[at..at + with.len()].copy_from_slice(with); + e +} + +#[test] +fn a_prior_whose_hash_is_not_the_pinned_one_is_refused() { + let env = envelope(); + assert!(open(&env).is_ok(), "the positive control opens"); + let bad = tamper(&env, b"a prior's bytes", b"A prior's bytes"); + assert_eq!(open(&bad).unwrap_err().code, "W014"); +} + +/// The code `env` is refused with, by a refusal that says `needle`: the rule this test +/// is of, and no other. +fn refusal(env: &[u8], needle: &str) -> &'static str { + let e = open(env).expect_err("the envelope opened"); + assert!(e.message.contains(needle), "refused for another rule than the one that says {needle:?}: {e}"); + e.code +} + +#[test] +fn a_changed_module_or_program_is_refused_for_its_digest() { + let env = envelope(); + assert_eq!(refusal(&tamper(&env, b"wasmq-prior", b"wasmq-PRIOR"), "module m: its bytes hash to"), "W014"); + assert_eq!(refusal(&tamper(&env, b"h q;", b"x q;"), "the program: its bytes hash to"), "W014"); +} + +#[test] +fn each_rule_of_the_envelope_s_form_refuses_what_breaks_it() { + use wai_wasmq::envelope::{MAGIC, from_parts, parts}; + let env = envelope(); + let (manifest, contract, frames) = parts(&env).unwrap(); + let text = String::from_utf8(contract.clone()).unwrap(); + let with = |manifest: &[u8], magic: &[u8; 4], contract: &[u8], frames: &[u8]| from_parts(manifest, magic, contract, frames).unwrap(); + assert_eq!(with(&manifest, MAGIC, &contract, &frames), env, "the parts, unchanged, are the envelope"); + let edit = |from: &str, to: &str| { + let edited = text.replacen(from, to, 1); + assert_ne!(edited, text, "{from} is in the contract"); + edited.into_bytes() + }; + // The contract's own run changed, canonical still: it no longer agrees with the manifest. + assert_eq!(refusal(&with(&manifest, MAGIC, &edit("\"shots\":4", "\"shots\":5"), &frames), "manifest"), "W013"); + // A member the profile does not write, in its sorted place, the lengths made right: + // another envelope for the same bundle (§2.4). + assert_eq!(refusal(&with(&manifest, MAGIC, &edit(",\"modules\":", ",\"extra\":0,\"modules\":"), &frames), "members this profile does not write"), "W013"); + // The same members, not in the canonical form: a space, and the keys in another order. + assert_eq!(refusal(&with(&manifest, MAGIC, &edit("{", "{ "), &frames), "canonical"), "W013"); + let v: serde_json::Value = serde_json::from_slice(&contract).unwrap(); + let reordered = format!("{{\"run\":{},\"callouts\":{},\"modules\":{},\"profile\":{},\"program\":{}}}", v["run"], v["callouts"], v["modules"], v["profile"], v["program"]); + assert_eq!(refusal(&with(&manifest, MAGIC, reordered.as_bytes(), &frames), "canonical"), "W013"); + // Another payload. + assert_eq!(refusal(&with(&manifest, b"WQM2", &contract, &frames), "not a WQM1 bundle"), "W013"); + // Frames that do not end where the payload does: a byte more, a byte less. + let mut more = frames.clone(); + more.push(0); + assert_eq!(refusal(&with(&manifest, MAGIC, &contract, &more), "after the frames"), "W013"); + assert_eq!(refusal(&with(&manifest, MAGIC, &contract, &frames[..frames.len() - 1]), "ends inside a frame"), "W013"); + // The manifest: a byte of it, and another capability. + let m = String::from_utf8(manifest.clone()).unwrap(); + assert_eq!(refusal(&with(m.replace("\"shots\":4", "\"shots\":5").as_bytes(), MAGIC, &contract, &frames), "manifest"), "W013"); + assert_eq!(refusal(&with(m.replace("wai.quantum.wasmq", "wai.quantum.wasmx").as_bytes(), MAGIC, &contract, &frames), "capability"), "W013"); + // The container: bytes after the payload, a byte short, another magic. + let mut long = env.clone(); + long.push(0); + assert_eq!(refusal(&long, "after the payload"), "W013"); + assert_eq!(refusal(&env[..env.len() - 1], "ends inside a frame"), "W013"); + assert_eq!(refusal(&tamper(&env, b"WAI1", b"WAI2"), "not a WAI1 envelope"), "W013"); +} + +#[test] +fn a_receipt_changed_after_sealing_or_held_to_another_bundle_does_not_verify() { + let b = bundle(wasm(IDENTITY), Run::new(8, 4)); + let o = open(&pack(&b).unwrap()).unwrap(); + let key = SigningKey::from_bytes(&[7; 32]); + let (body, _) = run(&o, EngineKind::Interpreter, &key, false).unwrap(); + assert!(wai_wasmq::verify(&body, Some(&o), EngineKind::Interpreter).unwrap().ok()); + let mut forged = body.clone(); + forged["result"]["fuel"]["used"] = serde_json::json!(1); + assert!(!wai_wasmq::verify(&forged, None, EngineKind::Interpreter).unwrap().signature); + let mut other = b.clone(); + other.run.seed = 5; + let o2 = open(&pack(&other).unwrap()).unwrap(); + assert_eq!(wai_wasmq::verify(&body, Some(&o2), EngineKind::Interpreter).unwrap().bundle, Some(false)); +} + +#[test] +fn without_its_meter_a_prepared_module_counts_nothing_and_without_its_stack_weight_it_is_not_bounded() { + let m = wasm(r#"(module (func $r (param i64) (result i64) local.get 0 i64.eqz if (result i64) i64.const 0 else local.get 0 i64.const 1 i64.sub call $r end) + (func (export "f") (param i64) (result i64) local.get 0 call $r))"#); + let go = |parts: Parts, arg: u64| { + let p = meter::prepare_with(&m, parts).unwrap(); + let e = Interpreter::new(&p, Metering::Module).unwrap(); + let mut i = e.instantiate().unwrap(); + i.set_fuel(1 << 40); + i.set_stack(0); + let r = i.call("f", &[arg]); + (r, (1i64 << 40) - i.fuel()) + }; + let (r, used) = go(Parts::ALL, 100); + assert!(r.is_ok() && used > 0); + assert_eq!(go(Parts { fuel: false, ..Parts::ALL }, 100).1, 0, "this test can fail: no meter, no fuel"); + // Deeper than the stack's weight lets a function of this size go. + let deep = 20_000; + assert_eq!(go(Parts::ALL, deep).0, Err(Trap::Stack)); + assert!(go(Parts { stack: false, ..Parts::ALL }, deep).0.is_ok(), "this test can fail: no weight, no bound"); +} diff --git a/wai/wai-wasmq/tests/engines.rs b/wai/wai-wasmq/tests/engines.rs new file mode 100644 index 000000000..42460c10e --- /dev/null +++ b/wai/wai-wasmq/tests/engines.rs @@ -0,0 +1,529 @@ +//! The engines agree (extensions/quantum-wasmq.md §4.6): every instruction of the profile +//! on edge-case operands gives the same value or the same trap on the interpreter (which +//! meters and canonicalises itself) and on the pinned native engine (which runs the +//! prepared module); the fuel each counts for a run is the fuel the native engine's own +//! meter counts on the original module, but for a `memory.grow` that fails; the stack +//! ends at the same call on each; and a native frame takes at most eight bytes for each +//! unit of the weight the profile gives it (§4.3). + +use wai_wasmq::engine::{Engine, Interpreter, Trap}; +use wai_wasmq::interp::Metering; +use wai_wasmq::meter; + +const I32: &[u64] = &[0, 1, 2, 7, 31, 32, 33, 0x7f, 0x80, 0xff, 0x7fff_ffff, 0x8000_0000, 0x8000_0001, 0xffff_ffff, 0xffff_fffe, 0x1234_5678]; +const I64: &[u64] = &[0, 1, 2, 63, 64, 65, 0xffff_ffff, 0x1_0000_0000, 0x7fff_ffff_ffff_ffff, 0x8000_0000_0000_0000, 0xffff_ffff_ffff_ffff, 0x0123_4567_89ab_cdef]; +const F32: &[u32] = &[ + 0x0000_0000, 0x8000_0000, 0x3f80_0000, 0xbf80_0000, 0x3f00_0000, 0x3fc0_0000, 0x4020_0000, 0xc020_0000, 0x7f80_0000, 0xff80_0000, 0x7fc0_0000, 0xffc0_0000, + 0x7fa0_0001, 0x7f80_0001, 0x7f7f_ffff, 0x0000_0001, 0x4f00_0000, 0xcf00_0000, 0x4f80_0000, 0x5f00_0000, 0xdf00_0000, 0xbf66_6666, 0x3eff_ffff, +]; +const F64: &[u64] = &[ + 0, 0x8000_0000_0000_0000, 0x3ff0_0000_0000_0000, 0xbff0_0000_0000_0000, 0x3fe0_0000_0000_0000, 0x3ff8_0000_0000_0000, 0x4004_0000_0000_0000, + 0xc004_0000_0000_0000, 0x7ff0_0000_0000_0000, 0xfff0_0000_0000_0000, 0x7ff8_0000_0000_0000, 0xfff8_0000_0000_0000, 0x7ff4_0000_0000_0001, + 0x7ff0_0000_0000_0001, 0x7fef_ffff_ffff_ffff, 1, 0x41e0_0000_0000_0000, 0xc1e0_0000_0000_0000, 0x41f0_0000_0000_0000, 0x43e0_0000_0000_0000, + 0xc3e0_0000_0000_0000, 0x43f0_0000_0000_0000, 0xbfec_cccc_cccc_cccd, 0x41df_ffff_ffc0_0000, +]; + +#[derive(Clone, Copy)] +enum T { + I32, + I64, + F32, + F64, +} + +impl T { + fn name(self) -> &'static str { + match self { + T::I32 => "i32", + T::I64 => "i64", + T::F32 => "f32", + T::F64 => "f64", + } + } + /// From the i64 parameter `k` to a value of this type. + fn load(self, k: usize) -> String { + match self { + T::I32 => format!("local.get {k} i32.wrap_i64"), + T::I64 => format!("local.get {k}"), + T::F32 => format!("local.get {k} i32.wrap_i64 f32.reinterpret_i32"), + T::F64 => format!("local.get {k} f64.reinterpret_i64"), + } + } + /// From a value of this type on the stack to the i64 result. + fn store(self) -> &'static str { + match self { + T::I32 => "i64.extend_i32_u", + T::I64 => "", + T::F32 => "i32.reinterpret_f32 i64.extend_i32_u", + T::F64 => "i64.reinterpret_f64", + } + } + fn values(self) -> Vec { + match self { + T::I32 => I32.to_vec(), + T::I64 => I64.to_vec(), + T::F32 => F32.iter().map(|&x| x as u64).collect(), + T::F64 => F64.to_vec(), + } + } +} + +/// Every instruction under test: its text, its operand types and its result type. +fn ops() -> Vec<(&'static str, Vec, T)> { + use T::*; + let mut v: Vec<(&'static str, Vec, T)> = Vec::new(); + for (t, names) in [ + (I32, &["add", "sub", "mul", "div_s", "div_u", "rem_s", "rem_u", "and", "or", "xor", "shl", "shr_s", "shr_u", "rotl", "rotr"][..]), + (I64, &["add", "sub", "mul", "div_s", "div_u", "rem_s", "rem_u", "and", "or", "xor", "shl", "shr_s", "shr_u", "rotl", "rotr"][..]), + (F32, &["add", "sub", "mul", "div", "min", "max", "copysign"][..]), + (F64, &["add", "sub", "mul", "div", "min", "max", "copysign"][..]), + ] { + for n in names { + v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t, t], t)); + } + } + for (t, names) in [(I32, &["eq", "ne", "lt_s", "lt_u", "gt_s", "gt_u", "le_s", "le_u", "ge_s", "ge_u"][..]), (I64, &["eq", "ne", "lt_s", "lt_u", "gt_s", "gt_u", "le_s", "le_u", "ge_s", "ge_u"][..]), (F32, &["eq", "ne", "lt", "gt", "le", "ge"][..]), (F64, &["eq", "ne", "lt", "gt", "le", "ge"][..])] { + for n in names { + v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t, t], I32)); + } + } + for (t, names) in [(I32, &["clz", "ctz", "popcnt", "extend8_s", "extend16_s"][..]), (I64, &["clz", "ctz", "popcnt", "extend8_s", "extend16_s", "extend32_s"][..]), (F32, &["abs", "neg", "ceil", "floor", "trunc", "nearest", "sqrt"][..]), (F64, &["abs", "neg", "ceil", "floor", "trunc", "nearest", "sqrt"][..])] { + for n in names { + v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t], t)); + } + } + v.push(("i32.eqz", vec![I32], I32)); + v.push(("i64.eqz", vec![I64], I32)); + for (name, from, to) in [ + ("i32.wrap_i64", I64, I32), ("i64.extend_i32_s", I32, I64), ("i64.extend_i32_u", I32, I64), + ("i32.trunc_f32_s", F32, I32), ("i32.trunc_f32_u", F32, I32), ("i32.trunc_f64_s", F64, I32), ("i32.trunc_f64_u", F64, I32), + ("i64.trunc_f32_s", F32, I64), ("i64.trunc_f32_u", F32, I64), ("i64.trunc_f64_s", F64, I64), ("i64.trunc_f64_u", F64, I64), + ("i32.trunc_sat_f32_s", F32, I32), ("i32.trunc_sat_f32_u", F32, I32), ("i32.trunc_sat_f64_s", F64, I32), ("i32.trunc_sat_f64_u", F64, I32), + ("i64.trunc_sat_f32_s", F32, I64), ("i64.trunc_sat_f32_u", F32, I64), ("i64.trunc_sat_f64_s", F64, I64), ("i64.trunc_sat_f64_u", F64, I64), + ("f32.convert_i32_s", I32, F32), ("f32.convert_i32_u", I32, F32), ("f32.convert_i64_s", I64, F32), ("f32.convert_i64_u", I64, F32), + ("f64.convert_i32_s", I32, F64), ("f64.convert_i32_u", I32, F64), ("f64.convert_i64_s", I64, F64), ("f64.convert_i64_u", I64, F64), + ("f32.demote_f64", F64, F32), ("f64.promote_f32", F32, F64), + ] { + v.push((name, vec![from], to)); + } + v +} + +fn module() -> (Vec, Vec<(String, Vec)>) { + let mut wat = String::from("(module (memory 1 1)\n"); + let mut names = Vec::new(); + for (k, (op, args, ret)) in ops().into_iter().enumerate() { + let params = "i64 ".repeat(args.len()); + let loads: Vec = args.iter().enumerate().map(|(i, t)| t.load(i)).collect(); + wat.push_str(&format!("(func (export \"op{k}\") (param {params}) (result i64) {} {op} {})\n", loads.join(" "), ret.store())); + names.push((format!("op{k}"), args)); + } + wat.push(')'); + (wat::parse_str(&wat).unwrap(), names) +} + +fn cases(args: &[T]) -> Vec> { + match args { + [a] => a.values().into_iter().map(|x| vec![x]).collect(), + [a, b] => a.values().into_iter().flat_map(|x| b.values().into_iter().map(move |y| vec![x, y])).collect(), + _ => vec![], + } +} + +fn run_all(engine: &dyn Engine, names: &[(String, Vec)]) -> Vec<(String, Result, Trap>, i64)> { + let mut i = engine.instantiate().unwrap(); + let mut out = Vec::new(); + for (name, args) in names { + for c in cases(args) { + i.set_fuel(1_000_000); + i.set_stack(0); + let r = i.call(name, &c); + out.push((format!("{name}{c:x?}"), r, i.fuel())); + } + } + out +} + +#[test] +fn every_instruction_gives_the_same_value_or_trap_on_both_engines() { + let (m, names) = module(); + let prepared = meter::prepare(&m).unwrap(); + let own = Interpreter::new(&m, Metering::Own).unwrap(); + let a = run_all(&own, &names); + let via = Interpreter::new(&prepared, Metering::Module).unwrap(); + let b = run_all(&via, &names); + assert_eq!(a.len(), b.len()); + assert!(a.len() > 5000, "{} cases", a.len()); + for (x, y) in a.iter().zip(&b) { + assert_eq!(x, y, "the interpreter on the original and on the prepared module"); + } + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + { + let native = wai_wasmq::engine::Native::new(&prepared).unwrap(); + let c = run_all(&native, &names); + for (x, y) in a.iter().zip(&c) { + assert_eq!(x, y, "the interpreter and the native engine"); + } + } + // Every NaN that comes out of arithmetic is canonical. + let traps = a.iter().filter(|x| x.1.is_err()).count(); + assert!(traps > 50, "{traps} traps: the edge cases reach the trapping paths"); +} + +/// Programs with every kind of control flow, memory traffic and calls. +const PROGRAMS: &str = r#" +(module (memory 1 4) + (table 3 funcref) (elem (i32.const 0) $sq $neg $sq) + (type $u (func (param i32) (result i32))) + (func $sq (type $u) local.get 0 local.get 0 i32.mul) + (func $neg (type $u) i32.const 0 local.get 0 i32.sub) + (func (export "loop") (param i64) (result i64) (local i32 i32) + local.get 0 i32.wrap_i64 local.set 1 + block loop + local.get 1 i32.eqz br_if 1 + local.get 2 local.get 1 local.get 1 i32.const 3 i32.rem_u call_indirect (type $u) i32.add local.set 2 + local.get 1 i32.const 1 i32.sub local.set 1 + br 0 + end end + local.get 2 i64.extend_i32_u) + (func (export "table") (param i64) (result i64) + block block block + local.get 0 i32.wrap_i64 br_table 0 1 2 + end i64.const 10 return + end i64.const 20 return + end i64.const 30) + (func $fib (param i32) (result i32) + local.get 0 i32.const 2 i32.lt_u if (result i32) local.get 0 else + local.get 0 i32.const 1 i32.sub call $fib local.get 0 i32.const 2 i32.sub call $fib i32.add end) + (func (export "fib") (param i64) (result i64) local.get 0 i32.wrap_i64 call $fib i64.extend_i32_u) + (func (export "mem") (param i64) (result i64) + i32.const 16 i32.const 7 local.get 0 i32.wrap_i64 memory.fill + i32.const 4096 i32.const 16 local.get 0 i32.wrap_i64 memory.copy + local.get 0 i32.wrap_i64 memory.grow drop + memory.size i64.extend_i32_u + i32.const 4100 i64.load8_u offset=0 i64.add) + (func $deep (param i32) (result i32) + local.get 0 i32.eqz if (result i32) i32.const 0 else local.get 0 i32.const 1 i32.sub call $deep i32.const 1 i32.add end) + (func (export "deep") (param i64) (result i64) local.get 0 i32.wrap_i64 call $deep i64.extend_i32_u) + (func (export "multi") (param i64) (result i64) + local.get 0 i64.const 1 block (param i64 i64) (result i64) i64.add end + i64.const 3 loop (param i64 i64) (result i64) i64.mul end)) +"#; + +fn fuel_of(engine: &dyn Engine, name: &str, arg: u64) -> (Result, Trap>, i64) { + let mut i = engine.instantiate().unwrap(); + i.set_fuel(100_000_000); + i.set_stack(0); + let r = i.call(name, &[arg]); + (r, 100_000_000 - i.fuel()) +} + +#[test] +fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engine_s_own_count_but_for_growth() { + let m = wat::parse_str(PROGRAMS).unwrap(); + let prepared = meter::prepare(&m).unwrap(); + let own = Interpreter::new(&m, Metering::Own).unwrap(); + let via = Interpreter::new(&prepared, Metering::Module).unwrap(); + // `mem(4)` asks a memory of one page and at most four for four more: the growth fails. + let calls: Vec<(&str, u64)> = vec![("loop", 0), ("loop", 1), ("loop", 50), ("table", 0), ("table", 1), ("table", 2), ("table", 9), ("fib", 0), ("fib", 15), ("mem", 0), ("mem", 1), ("mem", 3), ("mem", 4), ("deep", 10), ("deep", 1000), ("multi", 4)]; + for (name, arg) in &calls { + let a = fuel_of(&own, name, *arg); + let b = fuel_of(&via, name, *arg); + assert_eq!(a, b, "{name}({arg}): the interpreter, original and prepared"); + assert!(a.0.is_ok(), "{name}({arg}): {:?}", a.0); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + { + let native = wai_wasmq::engine::Native::new(&prepared).unwrap(); + assert_eq!(a, fuel_of(&native, name, *arg), "{name}({arg}): the native engine"); + let theirs = wai_wasmq::engine::native_fuel(&m, &[(name, vec![*arg])]).unwrap(); + // The profile charges `memory.grow` PAGE_FUEL for each page it asks for, whether + // or not it gets them (§4.2); the engine's own meter one for each page it gets. + let unmet = if *name == "mem" { meter::PAGE_FUEL * arg - if *arg < 4 { *arg } else { 0 } } else { 0 }; + assert_eq!((a.0.clone().unwrap(), a.1 as u64 - unmet), theirs[0].clone(), "{name}({arg}): the engine's own meter on the original module"); + } + } +} + +#[test] +fn a_run_that_runs_out_stops_at_the_same_charge_everywhere() { + let m = wat::parse_str(PROGRAMS).unwrap(); + let prepared = meter::prepare(&m).unwrap(); + let own = Interpreter::new(&m, Metering::Own).unwrap(); + let via = Interpreter::new(&prepared, Metering::Module).unwrap(); + for budget in [0i64, 1, 2, 5, 17, 100, 1234, 5000] { + for (name, arg) in [("loop", 30u64), ("fib", 12), ("mem", 3), ("deep", 200)] { + let go = |e: &dyn Engine| { + let mut i = e.instantiate().unwrap(); + i.set_fuel(budget); + i.set_stack(0); + let r = i.call(name, &[arg]); + (r, i.fuel()) + }; + let a = go(&own); + assert_eq!(a, go(&via), "{name}({arg}) at {budget}"); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + assert_eq!(a, go(&wai_wasmq::engine::Native::new(&prepared).unwrap()), "{name}({arg}) at {budget}, native"); + } + } +} + +/// The frame weights of a module's functions (§4.3). +fn weights(m: &[u8]) -> Vec { + wai_wasmq::profile::check(m).unwrap().weights.into_iter().map(u64::from).collect() +} + +#[test] +fn the_stack_ends_at_the_same_call_on_every_engine() { + let m = wat::parse_str(PROGRAMS).unwrap(); + let prepared = meter::prepare(&m).unwrap(); + // `deep(n)` suspends its own frame and `n` frames of `$deep`, functions 8 and 7. + let w = weights(&m); + let most = (wai_wasmq::profile::MAX_STACK as u64 - w[8]) / w[7]; + assert!(most > 10_000, "a small function recurses {most} deep"); + for (arg, ok) in [(most, true), (most + 1, false), (most * 2, false)] { + let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "deep", arg); + assert_eq!(own.0.is_ok(), ok, "deep({arg}): {:?}", own.0); + if !ok { + assert_eq!(own.0, Err(Trap::Stack)); + } + assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "deep", arg)); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "deep", arg)); + } +} + +/// The largest of `build(n)`, `n` below `past`, that the profile admits. +fn largest_admitted(past: usize, build: impl Fn(usize) -> Vec) -> (usize, Vec) { + let ok = |n: usize| wai_wasmq::profile::check(&build(n)).is_ok(); + let (mut lo, mut hi) = (1, past); + assert!(ok(lo) && !ok(hi), "the profile admits {past}"); + while hi - lo > 1 { + let mid = (lo + hi) / 2; + if ok(mid) { lo = mid } else { hi = mid } + } + (lo, build(lo)) +} + +/// Modules made to keep as much in a native frame as their bytes allow, each recursing +/// without end through its export `f`, with what each is: each at the largest body the +/// profile takes. +fn heavy_frames() -> Vec<(String, Vec)> { + let wasm = |wat: String| wat::parse_str(&wat).unwrap(); + let mut out = Vec::new(); + // Locals read from memory the callee goes on to write, all read again after the call: + // a compiler can neither read them again nor compute them there. + for ty in ["i64", "f64"] { + let (n, m) = largest_admitted(4000, |n| { + let mut body = String::from("i32.const 0 i32.const 0 i64.load i64.const 1 i64.add i64.store "); + for i in 0..n { + body.push_str(&format!("i32.const {} {ty}.load local.set {} ", 8 * (i % 1000), i + 1)); + } + body.push_str("local.get 0 call $f drop "); + for i in 1..=n { + body.push_str(&format!("local.get {i} {} ", if ty == "i64" { "local.get 0 i64.add local.set 0" } else { "f64.sqrt drop" })); + } + body.push_str("local.get 0"); + wasm(format!("(module (memory 1 1) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", format!("{ty} ").repeat(n))) + }); + out.push((format!("{n} {ty} locals held across a call"), m)); + } + // The review's module: forty parameters read at the end, and a new value held across + // two dozen calls at every step. + let (steps, m) = largest_admitted(8000, |steps| { + let mut body = String::from("local.get 0 i64.const 1 i64.add "); + for p in 1..40 { + body.push_str(&format!("local.get {p} ")); + } + body.push_str("call $f drop local.get 0 i64.const 99999999 i64.eq if "); + for k in 0..steps { + body.push_str(&format!("i32.const 0 i64.load local.set {} call $g local.get {} local.get 64 i64.add local.set 64 ", 40 + k % 24, 40 + (k + 1) % 24)); + } + body.push_str("end local.get 64 "); + for p in 0..40 { + body.push_str(&format!("local.get {p} i64.add ")); + } + let consts: String = (1..40).map(|p| format!("i64.const {p} ")).collect(); + wasm(format!( + "(module (memory 1 1) (func $g i32.const 0 i32.const 0 i64.load i64.const 1 i64.add i64.store) (func $f (param {}) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 {consts} call $f))", + "i64 ".repeat(40), + "i64 ".repeat(25) + )) + }); + out.push((format!("a value held across two dozen calls at each of {steps} steps"), m)); + // The cheapest value there is to make and keep: the memory's size, two bytes, held + // across calls in rotating locals. + let (steps, m) = largest_admitted(15_000, |steps| { + let mut body = String::from("local.get 0 call $f drop "); + for k in 0..steps { + body.push_str(&format!("memory.size local.set {} local.get {} call $h ", 1 + k % 24, 1 + (k + 1) % 24)); + } + body.push_str("local.get 0"); + wasm(format!("(module (memory 1 2) (func $h (param i32) local.get 0 memory.grow drop) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", "i32 ".repeat(24))) + }); + out.push((format!("the memory's size held across two dozen calls at each of {steps} steps"), m)); + // Results a callee leaves on the caller's operand stack, held there across the + // recursive call: fifteen calls of 256 results each, two bytes apiece, and 3840 values + // a frame (§4.3 counts the operands a function's body can hold, not only its bytes). + let ps = "call $p ".repeat(15); + let cs = "call $c ".repeat(15); + out.push(( + "3840 results of calls held across a call".to_string(), + wasm(format!( + "(module (func $p (result {0}) {1}) (func $c (param {0})) (func $f (export \"f\") (param i64) (result i64) {ps} local.get 0 i64.const 1 i64.add call $f drop {cs} local.get 0))", + "i64 ".repeat(256), + "i64.const 7 ".repeat(256) + )), + )); + // As many arguments and results as a function type takes, passed straight through. + let all: String = (0..256).map(|p| format!("local.get {p} ")).collect(); + out.push(( + "256 arguments and 256 results at every call".to_string(), + wasm(format!( + "(module (func $p (param {0}) (result {0}) {all} call $p) (func (export \"f\") (param i64) (result i64) {1} call $p {2}))", + "i64 ".repeat(256), + "local.get 0 ".repeat(256), + "drop ".repeat(255) + )), + )); + // The smallest frame there is. + out.push(("a function that only calls itself".to_string(), wasm("(module (func $f (export \"f\") (param i64) (result i64) local.get 0 call $f))".to_string()))); + out +} + +#[test] +fn a_native_frame_takes_at_most_eight_bytes_for_each_unit_of_its_weight() { + for (what, m) in heavy_frames() { + let prepared = meter::prepare(&m).unwrap(); + // The recursion has no end of its own: it stops where the stack's weight does. + let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", 0); + assert_eq!(own.0, Err(Trap::Stack), "{what}"); + assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", 0), "{what}: the interpreter on the prepared module"); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + { + // The frames suspended at calls weigh at most MAX_STACK, and one more frame + // runs on top of them. At eight bytes a unit, and 64 KiB for the engine's own + // entry, that is all the stack this engine is given here: half of what + // `Native::new` gives it. + let heaviest = *weights(&m).iter().max().unwrap() as usize; + let bytes = 8 * (wai_wasmq::profile::MAX_STACK as usize + heaviest) + (64 << 10); + let native = fuel_of(&wai_wasmq::engine::Native::with_stack(&prepared, bytes).unwrap(), "f", 0); + assert_eq!(own, native, "{what}: the native engine with {bytes} bytes of stack, where the frames weigh up to {heaviest}"); + } + } + // This test can fail: the frames are real. At one byte for each unit of weight, an + // eighth of that stack, the module of 256 arguments stops at the engine's limit and + // not at the profile's. + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + { + let (what, m) = heavy_frames().into_iter().find(|(what, _)| what.starts_with("256 arguments")).unwrap(); + let heaviest = *weights(&m).iter().max().unwrap() as usize; + let small = fuel_of(&wai_wasmq::engine::Native::with_stack(&meter::prepare(&m).unwrap(), wai_wasmq::profile::MAX_STACK as usize + heaviest + (64 << 10)).unwrap(), "f", 0); + assert!(matches!(small.0, Err(Trap::Host(_))), "{what}: {:?}", small.0); + } +} + +/// A module as large as the profile takes, its items as many as fit beside the largest body +/// there is, all calls: what preparing grows most. +fn at_the_limits() -> Vec { + use wai_wasmq::profile::{MAX_BODY_BYTES, MAX_EXPORTS, MAX_MODULE_BYTES, MAX_NAME_BYTES}; + let leb = |mut v: u64| { + let mut o = Vec::new(); + loop { + let b = (v & 0x7f) as u8; + v >>= 7; + if v == 0 { + o.push(b); + return o; + } + o.push(b | 0x80); + } + }; + let sec = |id: u8, body: Vec| { + let mut s = vec![id]; + s.extend(leb(body.len() as u64)); + s.extend(body); + s + }; + let n: u64 = 1 << 13; + // Types: `() -> ()`, and `(i64) -> i64` last. + let mut types = leb(n); + for _ in 0..n - 1 { + types.extend_from_slice(&[0x60, 0x00, 0x00]); + } + types.extend_from_slice(&[0x60, 0x01, 0x7e, 0x01, 0x7e]); + // Functions: the big one, `f` of `(i64) -> i64` that calls it, and empty ones. + let mut funcs = leb(n); + funcs.extend(leb(0)); + funcs.extend(leb(n - 1)); + funcs.extend(std::iter::repeat_n(0u8, (n - 2) as usize)); + let globals = { + let mut g = leb(n); + for _ in 0..n { + g.extend_from_slice(&[0x7f, 0x00, 0x41, 0x00, 0x0b]); + } + g + }; + // Exports: `f`, `memory`, and names of the longest length up to the most exports. + let mut exports = leb(u64::from(MAX_EXPORTS)); + exports.extend_from_slice(&[0x01, b'f', 0x00]); + exports.extend(leb(1)); + exports.extend_from_slice(&[0x06]); + exports.extend_from_slice(b"memory"); + exports.extend_from_slice(&[0x02, 0x00]); + for k in 0..u64::from(MAX_EXPORTS) - 2 { + let name = format!("{k:0>width$}", width = MAX_NAME_BYTES); + exports.extend(leb(name.len() as u64)); + exports.extend_from_slice(name.as_bytes()); + exports.extend_from_slice(&[0x00]); + exports.extend(leb(2 + k % (n - 2))); + } + // A table of the most entries, one segment of every function. + let table = vec![0x01, 0x70, 0x01, 0x80, 0x80, 0x04, 0x80, 0x80, 0x04]; + let mut elems = vec![0x01, 0x00, 0x41, 0x00, 0x0b]; + elems.extend(leb(n)); + for k in 0..n { + elems.extend(leb(k)); + } + // A memory of eight pages that may grow to the most. + let memory = vec![0x01, 0x01, 0x08, 0x80, 0x20]; + // The body: calls of an empty function up to the most bytes a body takes. + let mut big = vec![0x00]; + while (big.len() as u64) + 3 + 3 <= MAX_BODY_BYTES { + big.extend_from_slice(&[0x10, 0x02]); + } + big.push(0x0b); + let mut code = leb(n); + code.extend(leb(big.len() as u64)); + code.extend_from_slice(&big); + let f = [0x00, 0x10, 0x00, 0x20, 0x00, 0x0b]; + code.extend(leb(f.len() as u64)); + code.extend_from_slice(&f); + for _ in 0..n - 2 { + code.extend_from_slice(&[0x02, 0x00, 0x0b]); + } + let mut m = b"\0asm\x01\0\0\0".to_vec(); + for s in [sec(1, types), sec(3, funcs), sec(4, table), sec(5, memory), sec(6, globals), sec(7, exports), sec(9, elems), sec(10, code)] { + m.extend(s); + } + // The rest of the bytes a module takes, as data in its first pages. + let room = MAX_MODULE_BYTES - m.len() - 16; + let mut data = vec![0x01, 0x00, 0x41, 0x00, 0x0b]; + data.extend(leb(room as u64)); + data.extend(std::iter::repeat_n(0x5au8, room)); + m.extend(sec(11, data)); + m +} + +#[test] +fn a_module_at_the_profile_s_limits_prepares_compiles_and_runs_the_same_on_every_engine() { + let m = at_the_limits(); + assert!(m.len() > wai_wasmq::profile::MAX_MODULE_BYTES - 64, "{} bytes", m.len()); + wai_wasmq::profile::check(&m).unwrap(); + let prepared = meter::prepare(&m).unwrap(); + let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", 5); + assert!(own.0.is_ok(), "{:?}", own.0); + assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", 5), "the interpreter on the prepared module"); + // The native engine compiles the largest prepared body there is, and runs it alike. + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "f", 5), "the native engine"); +} diff --git a/wai/wai-wasmq/tests/fuzz.rs b/wai/wai-wasmq/tests/fuzz.rs new file mode 100644 index 000000000..65f2a964c --- /dev/null +++ b/wai/wai-wasmq/tests/fuzz.rs @@ -0,0 +1,375 @@ +//! A sweep for what a review found by reading (extensions/quantum-wasmq.md §3, §4): input +//! that panics a reader, and a module two engines run differently. Valid modules, envelopes +//! and programs are changed a byte at a time, tens of thousands of times from fixed seeds: +//! every reader must refuse or accept without a panic, what the profile accepts must +//! prepare, and every engine must give each export of an accepted module the same result, +//! the same fuel and the same fault. + +use wai_wasmq::engine::{Engine, Interpreter, Trap}; +use wai_wasmq::interp::Metering; +use wai_wasmq::{meter, profile}; + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) + } + fn below(&mut self, n: usize) -> usize { + (self.next() % n.max(1) as u64) as usize + } +} + +/// `bytes` with one change: a bit flipped, a byte set, inserted or removed, a stretch +/// copied from elsewhere in it, or its end cut off. +fn change(bytes: &[u8], rng: &mut Rng) -> Vec { + let mut b = bytes.to_vec(); + if b.is_empty() { + return b; + } + let at = rng.below(b.len()); + match rng.below(12) { + 0..=4 => b[at] ^= 1 << rng.below(8), + 5 | 6 => b[at] = rng.next() as u8, + 7 => b.insert(at, rng.next() as u8), + 8 => { + b.remove(at); + } + 9 | 10 => { + let from = rng.below(b.len()); + let n = rng.below(8) + 1; + for k in 0..n { + if at + k < b.len() && from + k < b.len() { + b[at + k] = b[from + k]; + } + } + } + _ => b.truncate(at), + } + b +} + +const CONTROL: &str = r#"(module (memory (export "memory") 1 4) + (table 3 funcref) (elem (i32.const 0) $sq $neg $sq) + (type $u (func (param i32) (result i32))) + (global $g (mut i64) (i64.const 7)) + (data (i32.const 8) "\01\02\03\04\05\06\07\08") + (func $sq (type $u) local.get 0 local.get 0 i32.mul) + (func $neg (type $u) i32.const 0 local.get 0 i32.sub) + (func (export "loop") (param i64) (result i64) (local i32 i32) + local.get 0 i32.wrap_i64 i32.const 63 i32.and local.set 1 + block loop + local.get 1 i32.eqz br_if 1 + local.get 2 local.get 1 local.get 1 i32.const 3 i32.rem_u call_indirect (type $u) i32.add local.set 2 + local.get 1 i32.const 1 i32.sub local.set 1 + br 0 + end end + local.get 2 i64.extend_i32_u global.get $g i64.add) + (func (export "table") (param i64) (result i64) + block block block + local.get 0 i32.wrap_i64 br_table 0 1 2 + end i64.const 10 return + end i64.const 20 global.set $g i64.const 21 return + end i64.const 30) + (func $fib (param i32) (result i32) + local.get 0 i32.const 2 i32.lt_u if (result i32) local.get 0 else + local.get 0 i32.const 1 i32.sub call $fib local.get 0 i32.const 2 i32.sub call $fib i32.add end) + (func (export "fib") (param i64) (result i64) local.get 0 i32.wrap_i64 i32.const 15 i32.and call $fib i64.extend_i32_u) + (func (export "mem") (param i64 i32) (result i64) + i32.const 16 i32.const 7 local.get 1 i32.const 255 i32.and memory.fill + i32.const 4096 i32.const 8 local.get 1 i32.const 255 i32.and memory.copy + local.get 0 i32.wrap_i64 i32.const 7 i32.and memory.grow drop + memory.size i64.extend_i32_u + local.get 1 i64.load8_u offset=4096 i64.add + local.get 1 i32.const 3 i32.shl local.get 0 i64.store + local.get 1 i64.load16_s i64.add) + (func (export "multi") (param i64 i64) (result i64) + local.get 0 local.get 1 block (param i64 i64) (result i64) i64.add end + i64.const 3 loop (param i64 i64) (result i64) i64.mul end + local.get 0 local.get 0 local.get 1 i64.lt_s select) + (func (export "echo") (param i32 f32 i64) (result f32 i32 i64) local.get 1 local.get 0 local.get 2) + (func (export "div") (param i64 i64 i32 i32) (result i64) + local.get 0 local.get 1 i64.div_s local.get 0 local.get 1 i64.rem_u i64.xor + local.get 2 local.get 3 i32.div_u local.get 2 local.get 3 i32.rem_s i32.rotl i64.extend_i32_s i64.add))"#; + +const FLOATS: &str = r#"(module (memory 1 1) + (func (export "mix") (param f64 f64 f32) (result i64) + local.get 0 local.get 1 f64.div local.get 0 local.get 1 f64.min f64.sub f64.sqrt + local.get 2 f64.promote_f32 f64.max f64.nearest i64.reinterpret_f64 + local.get 2 local.get 2 f32.mul f32.ceil local.get 0 f32.demote_f64 f32.copysign i32.reinterpret_f32 i64.extend_i32_u i64.xor) + (func (export "trunc") (param f64 f32) (result i64) + local.get 0 i64.trunc_f64_s local.get 1 i32.trunc_f32_u i64.extend_i32_u i64.add + local.get 0 i32.trunc_sat_f64_s i64.extend_i32_s i64.add) + (func (export "store") (param f64 i32) (result i64) + local.get 1 i32.const 1023 i32.and local.get 0 f64.store + local.get 1 i32.const 1023 i32.and f32.load f32.neg f32.floor i32.trunc_sat_f32_s i64.extend_i32_s + local.get 0 f64.abs f64.trunc i64.trunc_sat_f64_u i64.add))"#; + +fn outcome(e: &dyn Engine, name: &str, args: &[u64]) -> (Result, Trap>, i64) { + let mut i = match e.instantiate() { + Ok(i) => i, + Err(why) => return (Err(Trap::Host(why)), 0), + }; + i.set_fuel(50_000); + i.set_stack(0); + let r = i.call(name, args); + (r, i.fuel()) +} + +/// Changes `seed` `rounds` times, each from the original or from the last accepted +/// change; how many changed modules the profile accepted, and how many calls every +/// engine agreed on. +fn sweep(seed: &[u8], rounds: usize, rng: &mut Rng) -> (usize, usize) { + let (mut accepted, mut calls) = (0, 0); + let mut base = seed.to_vec(); + for round in 0..rounds { + if round % 16 == 0 { + base = seed.to_vec(); + } + let m = change(&base, rng); + let checked = std::panic::catch_unwind(|| profile::check(&m)); + let Ok(checked) = checked else { panic!("profile::check panicked on {m:02x?}") }; + let Ok(facts) = checked else { continue }; + accepted += 1; + // What the profile accepts, prepares; and every engine takes it. + let prepared = match std::panic::catch_unwind(|| meter::prepare(&m)) { + Ok(Ok(p)) => p, + Ok(Err(e)) => panic!("the profile accepts a module that does not prepare ({e}): {m:02x?}"), + Err(_) => panic!("meter::prepare panicked on {m:02x?}"), + }; + if facts.memory.is_some_and(|(min, _)| min > 64) { + continue; // a module of a large memory at once: nothing more to learn, and slow + } + let own = Interpreter::new(&m, Metering::Own).unwrap_or_else(|e| panic!("the interpreter refuses a module the profile accepts ({e}): {m:02x?}")); + let via = Interpreter::new(&prepared, Metering::Module).unwrap_or_else(|e| panic!("the interpreter refuses a prepared module ({e}): {m:02x?}")); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + let native = wai_wasmq::engine::Native::new(&prepared).unwrap_or_else(|e| panic!("the native engine refuses a prepared module ({e}): {m:02x?}")); + for (name, (params, _)) in &facts.functions { + if params.len() > 6 { + continue; + } + let args: Vec = params + .iter() + .map(|_| match rng.below(4) { + 0 => rng.below(40) as u64, + 1 => rng.next(), + 2 => 0x7ff8_0000_0000_0001 ^ (rng.next() & 0xffff), + _ => (rng.next() as u32 as u64) | (rng.next() << 40), + }) + .collect(); + let a = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| outcome(&own, name, &args))).unwrap_or_else(|_| panic!("the interpreter panicked running {name}{args:x?} of {m:02x?}")); + assert!(!matches!(a.0, Err(Trap::Host(_))), "the interpreter could not run {name}{args:x?}: {:?} in {m:02x?}", a.0); + let b = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| outcome(&via, name, &args))).unwrap_or_else(|_| panic!("the interpreter panicked running the prepared {name}{args:x?} of {m:02x?}")); + assert_eq!(a, b, "{name}{args:x?}: the interpreter on the module and on its prepared form differ, in {m:02x?}"); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + assert_eq!(a, outcome(&native, name, &args), "{name}{args:x?}: the interpreter and the native engine differ, in {m:02x?}"); + calls += 1; + } + // An accepted change is built on, so that changes meet. + if rng.below(3) == 0 { + base = m; + } + } + (accepted, calls) +} + +#[test] +fn a_changed_module_is_refused_or_runs_the_same_on_every_engine() { + let mut rng = Rng(0x0077_6173_6d51); + let (mut accepted, mut calls) = (0, 0); + for (seed, rounds) in [(wat::parse_str(CONTROL).unwrap(), 14_000), (wat::parse_str(FLOATS).unwrap(), 8_000)] { + // The seeds themselves are in the profile, and the engines agree on them. + if let Err(e) = profile::check(&seed) { + panic!("a seed is outside the profile: {e}"); + } + let (a, c) = sweep(&seed, rounds, &mut rng); + accepted += a; + calls += c; + } + // The sweep reached what it is for: thousands of changed modules inside the profile, + // and their exports run on every engine. + assert!(accepted > 2_000 && calls > 8_000, "{accepted} changed modules accepted, {calls} calls compared"); +} + +#[test] +fn a_changed_reference_module_is_refused_or_runs_the_same_on_every_engine() { + let mut rng = Rng(0x0072_6566); + let mut accepted = 0; + for file in ["qldpc_decoder.wasm", "atom_planner.wasm"] { + let seed = std::fs::read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq/modules").join(file)).unwrap(); + accepted += sweep(&seed, 300, &mut rng).0; + } + assert!(accepted > 20, "{accepted} changed modules accepted"); +} + +#[test] +fn a_changed_envelope_or_program_is_refused_without_a_panic() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq/envelopes"); + let mut rng = Rng(0x0065_6e76); + let (mut opened, mut refused) = (0, 0); + for file in ["surface3-decode64.wai", "list-echo.wai", "fault-fuel.wai"] { + let env = std::fs::read(dir.join(file)).unwrap(); + assert!(wai_wasmq::open(&env).is_ok()); + for _ in 0..4_000 { + let e = change(&env, &mut rng); + match std::panic::catch_unwind(|| wai_wasmq::open(&e)) { + Ok(Ok(o)) => { + // One envelope for one bundle (§2.4): what opens is the envelope of the + // bundle it opens as, byte for byte. (A change to a name or a budget + // makes another bundle, with an envelope of its own.) + assert!(wai_wasmq::pack(&o.bundle).is_ok_and(|again| again == e), "bytes that open as a bundle are not that bundle's envelope"); + opened += 1; + } + Ok(Err(_)) => refused += 1, + Err(_) => panic!("open panicked on a change of {file}"), + } + } + } + assert!(refused > 11_000, "{refused} refused, {opened} opened"); + // The program, as the reader with callouts takes it. + let program = std::fs::read(dir.join("surface3-decode64.qasm")).unwrap(); + let (mut read, mut not) = (0, 0); + for _ in 0..6_000 { + let p = change(&program, &mut rng); + let Ok(text) = String::from_utf8(p) else { continue }; + match std::panic::catch_unwind(|| wai_wasmq::envelope::read(&text, &Default::default())) { + Ok(Ok(_)) => read += 1, + Ok(Err(_)) => not += 1, + Err(_) => panic!("the reader panicked on {text:?}"), + } + } + assert!(read > 100 && not > 1_000, "{read} programs read, {not} refused"); +} + +/// A random place in a job, changed: replaced by a value of some other kind, or removed. +fn change_json(v: &mut serde_json::Value, rng: &mut Rng) { + use serde_json::{Value, json}; + let mut at = v; + loop { + let n = match &*at { + Value::Object(o) => o.len(), + Value::Array(a) => a.len(), + _ => 0, + }; + if n == 0 || rng.below(5) == 0 { + *at = match rng.below(12) { + 0 => Value::Null, + 1 => json!(true), + 2 => json!(0), + 3 => json!(-1), + 4 => json!(1u64 << 53), + 5 => json!(u64::MAX), + 6 => json!(1.5), + 7 => json!(""), + 8 => json!("x"), + 9 => json!([]), + 10 => json!({}), + _ => json!("0".repeat(64)), + }; + return; + } + let k = rng.below(n); + if rng.below(6) == 0 + && let Value::Object(o) = at + { + let name = o.keys().nth(k).unwrap().clone(); + o.remove(&name); + return; + } + at = match at { + Value::Object(o) => o.values_mut().nth(k).unwrap(), + Value::Array(a) => &mut a[k], + _ => unreachable!(), + }; + } +} + +fn sealed_again(job: &serde_json::Value) -> serde_json::Value { + let mut result = job["result"].clone(); + if let Some(o) = result.as_object_mut() { + o.remove("receipt"); + } + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[9; 32]); + let receipt = wai_quantum::engine::receipt::seal(&key, &job["request"], &result); + if result.is_object() { + result["receipt"] = receipt; + } + let status = if result["status"] == "ok" { "done" } else { "fault" }; + serde_json::json!({ "status": status, "method": result["method"], "request": job["request"], "result": result }) +} + +#[test] +fn a_changed_job_does_not_hold_with_its_envelope_and_no_job_or_record_panics() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + use wai_wasmq::run::{EngineKind, replay, run, verify}; + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq/envelopes"); + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[7; 32]); + let k = EngineKind::Interpreter; + let mut rng = Rng(0x6a6f_6272_6563); + let (mut as_edited, mut sealed) = (0, 0); + for file in ["list-echo.wai", "fault-fuel.wai", "fault-trap.wai", "reset-draws.wai"] { + let o = wai_wasmq::open(&std::fs::read(dir.join(file)).unwrap()).unwrap(); + let (job, log) = run(&o, k, &key, true).unwrap(); + assert!(verify(&job, Some(&o), k).unwrap().ok(), "{file}: the positive control"); + let same = sealed_again(&job); + assert!(verify(&same, Some(&o), k).unwrap().ok(), "{file}: sealed again unchanged, under another key"); + for _ in 0..500 { + let mut e = job.clone(); + change_json(&mut e, &mut rng); + if e == job { + continue; + } + // As edited: no signature, no form and no run lets a change of what is sealed + // through; a change of the host's unsealed words is noted, and decides nothing. + let v = catch_unwind(AssertUnwindSafe(|| verify(&e, Some(&o), k))).unwrap_or_else(|_| panic!("verify panicked on {e}")); + if e["request"] == job["request"] && e["result"] == job["result"] { + let v = v.unwrap(); + assert!(v.ok() && !v.notes.is_empty(), "{file}: an unsealed change is judged by what is sealed, and noted: {e}"); + } else { + assert!(!v.as_ref().is_ok_and(|v| v.ok()), "{file}: a changed job holds with its envelope: {e}"); + } + as_edited += 1; + let _ = catch_unwind(AssertUnwindSafe(|| verify(&e, None, k))).unwrap_or_else(|_| panic!("verify panicked on {e}")); + // Sealed again, under any key, it holds only if nothing signed changed. + let again = sealed_again(&e); + if again != same { + let v = catch_unwind(AssertUnwindSafe(|| verify(&again, Some(&o), k))).unwrap_or_else(|_| panic!("verify panicked on {again}")); + assert!(!v.as_ref().is_ok_and(|v| v.ok()), "{file}: a changed result, sealed again, holds with its envelope: {again}"); + let _ = catch_unwind(AssertUnwindSafe(|| verify(&again, None, k))).unwrap_or_else(|_| panic!("verify panicked on {again}")); + sealed += 1; + } + } + // A record, changed: refused or replayed, never a panic. + let log = log.unwrap(); + for _ in 0..500 { + let mut l = log.clone(); + if l.is_empty() { + break; + } + let at = rng.below(l.len()); + match rng.below(8) { + 0 => l[at].shot = rng.next(), + 1 => l[at].callout = "nobody".into(), + 2 => l[at].input = (0..rng.below(64)).map(|_| rng.next() as u8).collect(), + 3 => l[at].output = (0..rng.below(64)).map(|_| rng.next() as u8).collect(), + 4 => l[at].fuel = rng.next(), + 5 => { + l.remove(at); + } + 6 => { + let c = l[at].clone(); + l.insert(at, c); + } + _ => { + let other = rng.below(l.len()); + l.swap(at, other); + } + } + let _ = catch_unwind(AssertUnwindSafe(|| replay(&o, &l, k))).unwrap_or_else(|_| panic!("replay panicked on a changed record of {file}")); + } + } + assert!(as_edited > 1_500 && sealed > 500, "{as_edited} changes as edited, {sealed} sealed again"); +} diff --git a/wai/wai-wasmq/tests/reading.rs b/wai/wai-wasmq/tests/reading.rs new file mode 100644 index 000000000..4742276da --- /dev/null +++ b/wai/wai-wasmq/tests/reading.rs @@ -0,0 +1,113 @@ +//! A reader holds a program to the profile's bounds as it reads it (extensions/quantum-wasmq.md +//! §2.3, §4.8): a short program that would need more memory, stack or time to read than a host +//! has is refused, with little held, on every host. This file counts what is allocated. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; + +struct Counting; +static NOW: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + let p = unsafe { System.alloc(l) }; + if !p.is_null() { + PEAK.fetch_max(NOW.fetch_add(l.size(), Relaxed) + l.size(), Relaxed); + } + p + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + unsafe { System.dealloc(p, l) }; + NOW.fetch_sub(l.size(), Relaxed); + } + unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 { + let q = unsafe { System.realloc(p, l, n) }; + if !q.is_null() { + if n > l.size() { + PEAK.fetch_max(NOW.fetch_add(n - l.size(), Relaxed) + n - l.size(), Relaxed); + } else { + NOW.fetch_sub(l.size() - n, Relaxed); + } + } + q + } +} + +#[global_allocator] +static COUNTING: Counting = Counting; + +/// The most a refused program may make a reader hold, beyond what it held before: no more +/// than a program at the bounds does (some 232 MiB for two million gates: its tokens, its +/// gates and its steps). +const HELD: usize = 256 << 20; + +/// What reading `src` held at most, and its refusal's code and message. +fn read(src: &str) -> (usize, Option<(&'static str, String)>) { + let before = NOW.load(Relaxed); + PEAK.store(before, Relaxed); + let refused = wai_wasmq::envelope::read(src, &Default::default()).err().map(|e| (e.code, e.message)); + (PEAK.load(Relaxed) - before, refused) +} + +const HEAD: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[1] q;\nbit[1] d;\nd = f(d);\n"; + +/// Programs of a few hundred bytes that would each need more than a host has, and the +/// words of the bound each is refused by. +fn cases() -> Vec<(&'static str, String, &'static str)> { + let deep = |open: &str, close: &str| format!("{HEAD}int x = {}1{};\n", open.repeat(100_000), close.repeat(100_000)); + let wide = |body: &str| format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[32768] q;\nbit[32768] c;\nbit[1] d;\nd = f(d);\n{body}"); + vec![ + ("a register of 2^30 bits", format!("{HEAD}bit[1073741824] c;\n"), "bits; the program is held"), + ("bits past the program's", format!("{HEAD}bit[1048576] c;\n"), "bits; the program is held"), + ("calls whose bits add up past what a reader holds", "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1048575]) -> bit[1];\nqubit[1] q;\nbit[1048575] c;\nbit[1] d;\nfor uint i in [0:63] { d = f(c); }\n".into(), "the calls of externs"), + ("steps past the bound, from resets of every qubit", wide("for uint i in [0:9999] { reset q; }\n"), "steps it is held to"), + ("steps past the bound, from measurements of every qubit", wide("for uint i in [0:9999] { c = measure q; }\n"), "steps it is held to"), + ("steps past the bound, from gates on every qubit", wide("for uint i in [0:9999] { h q; }\n"), "gates"), + ("physical qubits past what the method holds", "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nbit[1] d;\nd = f(d);\nh $40000;\n".into(), "qubits"), + ("qubits past what the method holds", "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[32769] q;\nbit[1] d;\nd = f(d);\n".into(), "qubits; the program is held"), + ("a register past any reader's", "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[4294967295] q;\nbit[1] d;\nd = f(d);\n".into(), "qubits"), + ("an array of 2^26 elements", format!("{HEAD}array[int[8], 1024, 65536] a;\n"), "an array of"), + ("an array of a thousand registers of a million bits", format!("{HEAD}array[bit[1048575], 1024] a;\n"), "an array of"), + ("reading that does not finish its work", format!("{HEAD}bit[1048575] a;\nfor uint i in [0:199] {{ a = ~a; }}\n"), "units of work"), + ("brackets nested 100,000 deep", deep("(", ")"), "nest deeper"), + ("unary operators nested 100,000 deep", format!("{HEAD}int x = {}1;\n", "-".repeat(100_000)), "nest deeper"), + ("statements nested 50,000 deep", format!("{HEAD}{}x q[0];\n", "if (d == 1) ".repeat(50_000)), "nest deeper"), + ("blocks nested 100,000 deep", format!("{HEAD}{}x q[0];{}\n", "{ ".repeat(100_000), " }".repeat(100_000)), "nest deeper"), + ("a program of 2 MiB", format!("{HEAD}{}", "x q[0];\n".repeat(1 << 18)), "a reader takes at most"), + ] +} + +#[test] +fn a_program_past_the_bounds_is_refused_with_little_held() { + let mut wrong = Vec::new(); + for (what, src, why) in cases() { + let (held, refused) = read(&src); + match refused { + Some(("W016", message)) if message.contains(why) && held <= HELD => {} + other => wrong.push(format!("{what}: refused as {other:?}, holding {} MiB, where it is refused for `{why}`", held >> 20)), + } + } + assert!(wrong.is_empty(), "{wrong:#?}"); +} + +#[test] +fn a_program_at_the_bounds_is_read() { + // The deepest nesting the reader takes, and a program at the bounds of qubits, bits and + // steps: read, on every host (on wasm32 too, with its smaller stack). + let depth = wai_quantum::qasm::MAX_NESTING as usize; + let parens = format!("{HEAD}int x = {}1{};\n", "(".repeat(depth - 2), ")".repeat(depth - 2)); + assert!(read(&parens).1.is_none(), "{depth} deep"); + let unary = format!("{HEAD}int x = {}1;\n", "-".repeat(depth - 2)); + assert!(read(&unary).1.is_none(), "{depth} unary operators"); + // 125 `if`s, the last one's statement at 126, its operand at 127 and what it indexes at + // 128 (an index's indexed value is one deeper than it). + let ifs = format!("{HEAD}{}x q[0];\n", "if (d == 1) ".repeat(depth - 3)); + assert!(read(&ifs).1.is_none(), "{depth} deep through statements"); + let past = format!("{HEAD}{}x q[0];\n", "if (d == 1) ".repeat(depth - 2)); + assert!(read(&past).1.is_some_and(|(_, m)| m.contains("Q130")), "one more"); + let wide = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[32768] q;\nbit[1048575] c;\nbit[1] d;\nd = f(d);\nh q[0];\nc[0] = measure q[0];\n"; + let (held, refused) = read(wide); + assert!(refused.is_none(), "the most qubits and bits: {refused:?}"); + assert!(held < 256 << 20, "{} MiB", held >> 20); +} diff --git a/wai/wai-wasmq/tests/reference.rs b/wai/wai-wasmq/tests/reference.rs new file mode 100644 index 000000000..af3145a85 --- /dev/null +++ b/wai/wai-wasmq/tests/reference.rs @@ -0,0 +1,247 @@ +//! The reference modules (extensions/quantum-wasmq.md §8): the engine's qLDPC decoder and +//! its atom rearrangement planner, built as wasmQ modules of record, give exactly what the +//! same code gives natively, called directly and in the middle of a circuit, on every engine. + +use wai_quantum::quantum_atom::{Array, target_block, target_bytes}; +use wai_quantum::quantum_qec::CssCode; +use wai_wasmq::engine::{Engine, Interpreter}; +use wai_wasmq::envelope::{Run, open, pack}; +use wai_wasmq::interp::Metering; +use wai_wasmq::reference::{self, checks_prior, target_prior}; +use wai_wasmq::run::{EngineKind, run, shot_seed}; +use wai_wasmq::{abi, meter}; +use wai_quantum::ed25519_dalek::SigningKey; + +const DECODER: &[u8] = include_bytes!("../../quantum-conformance/wasmq/modules/qldpc_decoder.wasm"); +const PLANNER: &[u8] = include_bytes!("../../quantum-conformance/wasmq/modules/atom_planner.wasm"); + +fn engines(module: &[u8], prepared: &'static [u8]) -> Vec> { + let module: &'static [u8] = Box::leak(module.to_vec().into_boxed_slice()); + #[allow(unused_mut)] // the native engine is added where this build has it + let mut v: Vec> = vec![Box::new(Interpreter::new(module, Metering::Own).unwrap()), Box::new(Interpreter::new(prepared, Metering::Module).unwrap())]; + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + v.push(Box::new(wai_wasmq::engine::Native::new(prepared).unwrap())); + v +} + +fn bits(v: u64, n: usize) -> Vec { + (0..n).map(|i| v >> i & 1 == 1).collect() +} + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) + } +} + +#[test] +fn the_priors_are_the_engines_own_bytes() { + for code in [CssCode::surface(3), CssCode::surface(5), CssCode::gross()] { + assert_eq!(checks_prior(code.n, &code.checks), code.check_matrix_bytes()); + } + assert_eq!(target_prior(8, 8, &target_block(8, 8, 4, 4)), target_bytes(&target_block(8, 8, 4, 4), 8, 8)); + assert_eq!(reference::target_block(8, 8, 4, 4), target_block(8, 8, 4, 4)); +} + +fn decoder_instance<'a>(e: &'a dyn Engine, code: &CssCode, seed: u64) -> Box { + let mut i = e.instantiate().unwrap(); + i.set_fuel(1 << 40); + abi::lifecycle(i.as_mut(), "wasmq-seed", &[seed]).unwrap(); + abi::prior(i.as_mut(), 0, &checks_prior(code.n, &code.checks)).unwrap(); + i +} + +#[test] +fn the_decoder_module_decodes_as_the_engine_does_on_every_engine() { + let prepared: &'static [u8] = Box::leak(meter::prepare(DECODER).unwrap().into_boxed_slice()); + let small = CssCode::surface(3); + let decl = wai_quantum::qasm::ExternDecl { name: "decode64".into(), args: vec![wai_quantum::qasm::ExternArg::Bit(small.checks.len() as u32)], ret: Some(small.n as u32) }; + let seed = 11; + // Every error of weight at most two on surface(3). + let mut errors = vec![vec![]]; + for a in 0..small.n { + errors.push(vec![a]); + for b in a + 1..small.n { + errors.push(vec![a, b]); + } + } + let mut fuels: Vec> = Vec::new(); + for e in engines(DECODER, prepared) { + let mut i = decoder_instance(e.as_ref(), &small, seed); + let mut f = Vec::new(); + for err in &errors { + let mut x = vec![false; small.n]; + for &q in err { + x[q] = true; + } + let syn = small.syndrome(&x); + i.set_fuel(1 << 30); + let got = abi::call(i.as_mut(), &decl, "decode64", &syn, false).unwrap(); + f.push((1u64 << 30) - i.fuel() as u64); + assert_eq!(got, wasmq_qldpc_decoder::decode_bits(&small, &syn, seed), "{} on {err:?}", e.name()); + if err.len() <= 1 { + let residual: Vec = (0..small.n).filter(|&q| got[q] != x[q]).collect(); + assert!(small.syndrome(&(0..small.n).map(|q| got[q] != x[q]).collect::>()).iter().all(|b| !b)); + assert!(small.is_trivial(&residual), "a weight-1 error decoded to a logical"); + } + } + fuels.push(f); + } + assert!(fuels.windows(2).all(|w| w[0] == w[1]), "each call's fuel is the same on every engine"); + + // The gross code through the list form, with random errors of weight up to four. + let gross = CssCode::gross(); + let decl = wai_quantum::qasm::ExternDecl { name: "decode".into(), args: vec![wai_quantum::qasm::ExternArg::Bit(gross.checks.len() as u32)], ret: Some(gross.n as u32) }; + let mut rng = Rng(5); + let cases: Vec> = (0..12) + .map(|_| { + let mut x = vec![false; gross.n]; + for _ in 0..1 + rng.next() % 4 { + x[(rng.next() % gross.n as u64) as usize] = true; + } + gross.syndrome(&x) + }) + .collect(); + let mut fuels: Vec> = Vec::new(); + for e in engines(DECODER, prepared) { + let mut i = decoder_instance(e.as_ref(), &gross, seed); + let mut f = Vec::new(); + for syn in &cases { + i.set_fuel(1 << 34); + let got = abi::call(i.as_mut(), &decl, "decode", syn, false).unwrap(); + f.push((1u64 << 34) - i.fuel() as u64); + assert_eq!(got, wasmq_qldpc_decoder::decode_bits(&gross, syn, seed), "{}", e.name()); + } + fuels.push(f); + } + assert!(fuels.windows(2).all(|w| w[0] == w[1])); +} + +#[test] +fn the_planner_module_plans_as_the_engine_does_on_every_engine() { + let prepared: &'static [u8] = Box::leak(meter::prepare(PLANNER).unwrap().into_boxed_slice()); + for (w, h, tw, th, f) in [(4u32, 4u32, 2u32, 2u32, "plan64"), (5, 5, 3, 3, "plan64"), (9, 9, 5, 5, "plan")] { + let n = (w * h) as usize; + let target = target_block(w, h, tw, th); + let decl = wai_quantum::qasm::ExternDecl { name: f.into(), args: vec![wai_quantum::qasm::ExternArg::Bit(n as u32)], ret: Some(2 * n as u32 + 1) }; + let mut rng = Rng(w as u64); + let loads: Vec> = (0..16).map(|k| Array::load(w, h, 300 + 40 * k, rng.next()).occ).collect(); + let mut fuels: Vec> = Vec::new(); + for e in engines(PLANNER, prepared) { + let mut i = e.instantiate().unwrap(); + i.set_fuel(1 << 40); + abi::prior(i.as_mut(), 0, &target_prior(w, h, &target)).unwrap(); + let mut fu = Vec::new(); + for occ in &loads { + i.set_fuel(1 << 34); + let got = abi::call(i.as_mut(), &decl, f, occ, false).unwrap(); + fu.push((1u64 << 34) - i.fuel() as u64); + assert_eq!(got, wasmq_atom_planner::plan_bits(w, h, &target, occ), "{} {w}x{h}", e.name()); + // The engine's own check of the plan, on what the module returned. + let plan = wai_quantum::quantum_atom::solve(&Array { w, h, occ: occ.clone() }, &target); + assert_eq!(got[2 * n], plan.feasible && wai_quantum::quantum_atom::verify(&Array { w, h, occ: occ.clone() }, &target, &plan)); + } + fuels.push(fu); + } + assert!(fuels.windows(2).all(|w| w[0] == w[1])); + } +} + +fn kinds() -> Vec { + #[allow(unused_mut)] // the native engine is added where this build has it + let mut k = vec![EngineKind::Interpreter, EngineKind::InterpreterPrepared]; + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + k.insert(0, EngineKind::Native); + k +} + +#[test] +fn a_memory_round_decodes_in_the_middle_of_the_circuit_and_every_shot_ends_in_the_code() { + let key = SigningKey::from_bytes(&[7; 32]); + for (code, errors, shots, state) in [(CssCode::surface(3), 1, 48, "shot"), (CssCode::surface(5), 2, 24, "job"), (CssCode::gross(), 3, 6, "shot")] { + let mut r = Run::new(shots, 1234); + r.state = state.into(); + let env = pack(&reference::qec_bundle(DECODER, code.n, &code.checks, errors, r)).unwrap(); + let o = open(&env).unwrap(); + let mut bodies = Vec::new(); + for k in kinds() { + let (body, log) = run(&o, k, &key, true).unwrap(); + let log = log.unwrap(); + assert_eq!(body["result"]["status"], "ok", "{}: {}", code.id, body["result"]); + // Every call's result is the native decoder's on the same syndrome. + for c in &log { + let syn: Vec = (0..code.checks.len()).map(|i| c.input[i / 8] >> (i % 8) & 1 == 1).collect(); + let seed = if state == "shot" { shot_seed(1234, c.shot) } else { 1234 }; + let want = abi::pack(&wasmq_qldpc_decoder::decode_bits(&code, &syn, seed)); + assert_eq!(c.output, want, "shot {} of {}", c.shot, code.id); + } + assert_eq!(log.len() as u64, shots); + // The record replays: every call's output and fuel again, from its input alone. + assert!(wai_wasmq::run::replay(&o, &log, k).unwrap().is_empty()); + let mut bad = log.clone(); + bad[0].output[0] ^= 1; + bad[1].fuel += 1; + assert_eq!(wai_wasmq::run::replay(&o, &bad, k).unwrap().len(), 2, "this test can fail"); + bodies.push(wai_quantum::engine::receipt::canonical(&body)); + } + assert!(bodies.windows(2).all(|w| w[0] == w[1]), "{}: the same job bytes on every engine", code.id); + let body: serde_json::Value = serde_json::from_slice(&bodies[0]).unwrap(); + // Every shot: the second syndrome is clear and the residual is no logical. + let (n, m) = (code.n, code.checks.len()); + for (key, count) in body["result"]["counts"].as_object().unwrap() { + let bit = |i: usize| key.as_bytes()[key.len() - 1 - i] == b'1'; + let k = key.len(); + // Bits in declaration order: e*, s, corr, s2, fin; fin are the last n declared. + let fin: Vec = (k - n..k).map(bit).collect(); + let s2: Vec = (k - n - m..k - n).map(bit).collect(); + assert!(s2.iter().all(|b| !b), "{}: the syndrome after correction ({count} shots)", code.id); + let residual: Vec = (0..n).filter(|&q| fin[q]).collect(); + assert!(code.syndrome(&fin).iter().all(|b| !b)); + assert!(code.is_trivial(&residual), "{}: a shot ended in a logical error", code.id); + } + assert!(wai_wasmq::verify(&body, Some(&o), kinds()[0]).unwrap().ok()); + } +} + +#[test] +fn atoms_are_rearranged_in_the_middle_of_the_circuit_and_fill_the_target() { + let key = SigningKey::from_bytes(&[7; 32]); + for (w, h, tw, th, shots) in [(4u32, 4u32, 2u32, 2u32, 64u64), (9, 9, 3, 3, 12)] { + let env = pack(&reference::atom_bundle(PLANNER, w, h, tw, th, Run::new(shots, 99))).unwrap(); + let o = open(&env).unwrap(); + let target = target_block(w, h, tw, th); + let n = (w * h) as usize; + let mut bodies = Vec::new(); + for k in kinds() { + let (body, log) = run(&o, k, &key, true).unwrap(); + for c in log.unwrap() { + let occ: Vec = (0..n).map(|i| c.input[i / 8] >> (i % 8) & 1 == 1).collect(); + assert_eq!(c.output, abi::pack(&wasmq_atom_planner::plan_bits(w, h, &target, &occ))); + } + bodies.push(wai_quantum::engine::receipt::canonical(&body)); + } + assert!(bodies.windows(2).all(|w| w[0] == w[1])); + let body: serde_json::Value = serde_json::from_slice(&bodies[0]).unwrap(); + let mut feasible = 0; + for (key, count) in body["result"]["counts"].as_object().unwrap() { + let bit = |i: usize| key.as_bytes()[key.len() - 1 - i] == b'1'; + let k = key.len(); + // Declared: occ, plan, fin. + let plan_ok = bit(n + 2 * n); + let fin: Vec = (k - n..k).map(bit).collect(); + let occ: Vec = (0..n).map(bit).collect(); + if plan_ok { + feasible += count.as_u64().unwrap(); + assert!(target.iter().all(|&t| fin[t]), "a feasible plan left a target site empty"); + assert_eq!(fin.iter().filter(|&&b| b).count(), occ.iter().filter(|&&b| b).count(), "atoms are moved, never made or lost"); + } + } + assert!(feasible > 0); + let _ = bits(0, 0); + } +} diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs new file mode 100644 index 000000000..0aa849344 --- /dev/null +++ b/wai/wai-wasmq/tests/review.rs @@ -0,0 +1,665 @@ +//! What a review of the profile found, each as a test that failed before its fix +//! (extensions/quantum-wasmq.md): engines that reached different results for one envelope, +//! members of a job no signature covered, input that panicked a reader instead of being +//! refused, and bundles that packed and then could not run. + +use serde_json::{Value, json}; +use wai_quantum::ed25519_dalek::SigningKey; +use wai_quantum::qasm::{ExternArg, ExternDecl}; +use wai_wasmq::abi::{self, Stop}; +use wai_wasmq::engine::{Engine, Interpreter, Trap}; +use wai_wasmq::envelope::{Bundle, Callout, Module, Run, open, pack, write_unchecked}; +use wai_wasmq::interp::Metering; +use wai_wasmq::run::{EngineKind, run}; +use wai_wasmq::{meter, profile}; + +fn wasm(wat: &str) -> Vec { + wat::parse_str(wat).unwrap() +} + +fn leb(mut v: u64) -> Vec { + let mut out = Vec::new(); + loop { + let b = (v & 0x7f) as u8; + v >>= 7; + if v == 0 { + out.push(b); + return out; + } + out.push(b | 0x80); + } +} + +fn section(id: u8, body: &[u8]) -> Vec { + let mut s = vec![id]; + s.extend(leb(body.len() as u64)); + s.extend_from_slice(body); + s +} + +fn raw(sections: &[Vec]) -> Vec { + let mut m = b"\0asm\x01\0\0\0".to_vec(); + for s in sections { + m.extend_from_slice(s); + } + m +} + +/// One type `() -> ()`. +fn unit_type() -> Vec { + section(1, &[0x01, 0x60, 0x00, 0x00]) +} + +/// `n` functions of type 0, each an empty body. +fn functions(n: u64) -> (Vec, Vec) { + let mut f = leb(n); + f.extend(std::iter::repeat_n(0u8, n as usize)); + let mut c = leb(n); + for _ in 0..n { + c.extend_from_slice(&[0x02, 0x00, 0x0b]); + } + (section(3, &f), section(10, &c)) +} + +fn kinds() -> Vec { + wai_wasmq::engine::available().iter().map(|s| EngineKind::parse(s).unwrap()).collect() +} + +/// Every engine of this build on `module`: the interpreter on the module as written and on +/// its prepared form, and the native engine where the build has it. +fn engines(module: &[u8]) -> Vec> { + let module: &'static [u8] = Box::leak(module.to_vec().into_boxed_slice()); + let prepared: &'static [u8] = Box::leak(meter::prepare(module).unwrap().into_boxed_slice()); + #[allow(unused_mut)] // the native engine is added where this build has it + let mut v: Vec> = vec![Box::new(Interpreter::new(module, Metering::Own).unwrap()), Box::new(Interpreter::new(prepared, Metering::Module).unwrap())]; + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + v.push(Box::new(wai_wasmq::engine::Native::new(prepared).unwrap())); + v +} + +const SCALAR: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nh q;\nb = measure q;\nr = f(b);\n"; +const LIST: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[72]) -> bit[72];\nqubit[72] q;\nbit[72] b;\nbit[72] r;\nh q;\nb = measure q;\nr = f(b);\n"; + +fn bundle(program: &str, module: Vec, priors: Vec<(String, Vec)>) -> Bundle { + Bundle { program: program.into(), inputs: Default::default(), modules: vec![Module { name: "m".into(), bytes: module, priors }], callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], run: Run::new(4, 1) } +} + +fn code_of(r: Result) -> &'static str { + match r { + Ok(_) => "accepted", + Err(e) => e.code, + } +} + +/// How a call ended, in words that compare. +fn outcome(r: Result) -> String { + match r { + Ok(v) => format!("ok {v:?}"), + Err(Stop::Fault(f)) => format!("fault {}", f.reason), + Err(Stop::Host(_)) => "host".into(), + } +} + +/// Runs an opened bundle on every engine of the build: each job's canonical bytes, or why +/// there is none. +fn everywhere(o: &wai_wasmq::Opened) -> Vec { + let key = SigningKey::from_bytes(&[7; 32]); + kinds() + .into_iter() + .map(|k| match run(o, k, &key, false) { + Ok((v, _)) => format!("{}", String::from_utf8_lossy(&wai_quantum::engine::receipt::canonical(&v))), + Err(e) => format!("no receipt on {k:?}: {e}"), + }) + .collect() +} + +// --------------------------------------------------------------------------- +// 1. One envelope, one receipt: the memory a host reaches +// --------------------------------------------------------------------------- + +/// A list-form callout whose module declares a memory and does not export it. +const UNEXPORTED: &str = r#"(module (memory 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "f") (param i32 i32) (result i32) + i32.const 2048 local.get 0 i32.store i32.const 2052 local.get 1 i32.store i32.const 2048))"#; + +#[test] +fn every_engine_reaches_a_module_s_memory_the_same_way() { + let decl = ExternDecl { name: "f".into(), args: vec![ExternArg::Bit(72)], ret: Some(72) }; + let input = vec![true; 72]; + for (what, module) in [("not exported", wasm(UNEXPORTED)), ("exported", wasm(&UNEXPORTED.replace("(memory 1 1)", "(memory (export \"memory\") 1 1)")))] { + // How the call ended, and the fuel it left: an engine that got further than + // another before it stopped has used more. + let got: Vec<(String, i64)> = engines(&module) + .iter() + .map(|e| { + let mut i = e.instantiate().unwrap(); + i.set_fuel(1 << 30); + let r = outcome(abi::call(i.as_mut(), &decl, "f", &input, false)); + (r, i.fuel()) + }) + .collect(); + assert!(got.windows(2).all(|w| w[0] == w[1]), "a memory that is {what}: the engines differ: {got:?}"); + assert!(got[0].0.starts_with(if what == "exported" { "ok " } else { "fault bad_result" }), "{got:?}"); + } +} + +#[test] +fn a_bundle_whose_memory_the_host_cannot_reach_is_refused_on_reading() { + // A list-form callout. + assert_eq!(code_of(pack(&bundle(LIST, wasm(UNEXPORTED), vec![]))), "W015"); + // A prior, with a scalar callout. + let priors = r#"(module (memory 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "wasmq-prior") (param i32 i32 i32)) + (func (export "f") (param i64) (result i64) local.get 0))"#; + assert_eq!(code_of(pack(&bundle(SCALAR, wasm(priors), vec![("p".into(), b"prior".to_vec())]))), "W015"); + // The positive control: the same two modules with the memory exported pack and run. + for (program, module, pr) in [ + (LIST, UNEXPORTED.replace("(memory 1 1)", "(memory (export \"memory\") 1 1)"), vec![]), + (SCALAR, priors.replace("(memory 1 1)", "(memory (export \"memory\") 1 1)"), vec![("p".to_string(), b"prior".to_vec())]), + ] { + let env = pack(&bundle(program, wasm(&module), pr)).unwrap(); + let jobs = everywhere(&open(&env).unwrap()); + assert!(jobs.windows(2).all(|w| w[0] == w[1]) && jobs[0].contains("\"status\":\"done\""), "{jobs:?}"); + } +} + +// --------------------------------------------------------------------------- +// 2. Nothing a reader takes from a job is outside the signature +// --------------------------------------------------------------------------- + +fn a_job(module: &str, per_call: u64) -> (wai_wasmq::Opened, Value) { + let mut b = bundle(SCALAR, wasm(module), vec![]); + b.run.per_call = per_call; + let o = open(&pack(&b).unwrap()).unwrap(); + let (body, _) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), false).unwrap(); + (o, body) +} + +const IDENTITY: &str = r#"(module (func (export "f") (param i64) (result i64) local.get 0))"#; +const SPIN: &str = r#"(module (func (export "f") (param i64) (result i64) loop br 0 end i64.const 0))"#; + +/// A program past the controls the reader writes in standard gates is refused, and the +/// refusal offers nothing a hybrid program cannot use: its run is on the tableau alone. +#[test] +fn a_refusal_for_controls_offers_no_method_a_hybrid_program_cannot_use() { + let program = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[18] q;\nbit[1] d;\nctrl(17) @ x q[0], q[1], q[2], q[3], q[4], q[5], q[6], q[7], q[8], q[9], q[10], q[11], q[12], q[13], q[14], q[15], q[16], q[17];\nd = f(d);\n"; + let b = Bundle { program: program.into(), inputs: Default::default(), modules: vec![Module { name: "m".into(), bytes: wasm(IDENTITY), priors: vec![] }], callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], run: Run::new(4, 1) }; + let e = pack(&b).expect_err("17 controls"); + assert!(e.code == "W016" && e.message.contains("Q208"), "{e}"); + assert!(!e.message.contains("state vector") && !e.message.contains("bit-exact"), "{e}"); +} + +/// Each budget is at most 2^39 (§2.2, §4.8), as every verifier holds a contract to it: at +/// the bound a run packs, runs and verifies, and one past it is refused when packed, so the +/// writer never seals a job its verifiers refuse. +#[test] +fn a_budget_past_what_a_run_may_be_given_is_refused_when_packed() { + use wai_wasmq::envelope::MAX_BUDGET; + assert_eq!(MAX_BUDGET, 1 << 39); + type Set = fn(&mut Bundle, u64); + let sets: [(&str, Set); 3] = [("per call", |b, v| b.run.per_call = v), ("per instance", |b, v| b.run.per_init = v), ("per run", |b, v| b.run.per_job = v)]; + for (what, set) in sets { + let mut b = bundle(SCALAR, wasm(IDENTITY), vec![]); + set(&mut b, MAX_BUDGET + 1); + let e = pack(&b).expect_err(what); + assert!(e.code == "W017" && e.message.contains("past 2^39"), "{what}: {e}"); + if what == "per run" { + continue; // a run given 2^39 asks more than a host has (§4.8), and is refused for its work + } + set(&mut b, MAX_BUDGET); + let o = open(&pack(&b).unwrap_or_else(|e| panic!("{what}: {e}"))).unwrap(); + let (body, _) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), false).unwrap(); + let v = wai_wasmq::verify(&body, Some(&o), EngineKind::Interpreter).unwrap(); + assert!(v.ok(), "{what} at 2^39: {:?}", v.well_formed); + } +} + +/// A job whose contract a JSON writer carried and wrote otherwise (4.0 for 4) is the same +/// sealed content, its receipt the same: given its envelope, the envelope is the one it binds +/// and runs again to the same result, as every reader of the job finds. +#[test] +fn a_contract_written_otherwise_is_the_same_bundle() { + let (o, done) = a_job(IDENTITY, 500); + let mut respelt = done.clone(); + let shots = respelt["request"]["wasmq"]["run"]["shots"].as_u64().unwrap(); + respelt["request"]["wasmq"]["run"]["shots"] = json!(shots as f64); + assert_ne!(respelt, done, "a number written another way is another value to a reader that keeps its spelling"); + let v = wai_wasmq::verify(&respelt, Some(&o), EngineKind::Interpreter).unwrap(); + assert!(v.signature && v.well_formed.is_empty(), "{:?}", v.well_formed); + assert_eq!((v.bundle, v.reproduced), (Some(true), Some(true))); +} + +#[test] +fn a_job_whose_status_or_method_was_edited_outside_the_signature_is_judged_by_what_is_sealed_and_noted() { + // The verdict is the sealed content's, on every surface alike (§6.5): the host's + // unsealed words decide nothing, and where they differ from what is sealed a reader is + // told, beside the verdict. The outcome a check reports is the sealed result's. + let (o, fault) = a_job(SPIN, 500); + assert_eq!(fault["status"], "fault"); + let v = wai_wasmq::verify(&fault, Some(&o), EngineKind::Interpreter).unwrap(); + assert!(v.ok() && v.notes.is_empty(), "the positive control: {:?}", v.notes); + let mut edited = fault.clone(); + edited["status"] = json!("done"); + let v = wai_wasmq::verify(&edited, Some(&o), EngineKind::Interpreter).unwrap(); + assert!(v.ok(), "a fault edited to done is the sealed fault's verdict"); + assert_eq!(v.outcome.as_deref(), Some("fault"), "the outcome is the sealed result's"); + assert_eq!(v.notes, vec![r#"the host's unsealed status says "done"; the sealed result says "fault""#.to_string()]); + let (o, done) = a_job(IDENTITY, 500); + for (member, value, note) in [ + ("status", json!("fault"), r#"the host's unsealed status says "fault"; the sealed result says "done""#), + ("method", json!("statevector"), r#"the host's unsealed method says "statevector"; the sealed result says "stabilizer""#), + ("note", json!("a member nothing signs"), "the host's body holds `note`, which nothing seals"), + ] { + let mut edited = done.clone(); + edited[member] = value; + for bundle in [Some(&o), None] { + let v = wai_wasmq::verify(&edited, bundle, EngineKind::Interpreter).unwrap(); + assert!(v.ok(), "the body's `{member}` changed: {:?}", v.well_formed); + assert_eq!(v.notes, vec![note.to_string()], "the body's `{member}` changed"); + } + } + let mut bare = done.clone(); + bare.as_object_mut().unwrap().remove("status"); + assert_eq!(wai_wasmq::verify(&bare, None, EngineKind::Interpreter).unwrap().notes, vec!["the host's body has no `status`; the sealed result gives it".to_string()]); +} + +#[test] +fn a_job_names_its_signer_and_a_job_sealed_again_names_another() { + // A receipt shows that a key sealed a result; who holds the key is the reader's to + // compare. So a check names the key, and a result changed and sealed again under + // another key shows as another signer. + let (o, job) = a_job(IDENTITY, 500); + let genuine = wai_wasmq::verify(&job, None, EngineKind::Interpreter).unwrap(); + let demo = SigningKey::from_bytes(&[7; 32]); + let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + assert_eq!(genuine.signer.as_deref(), Some(hex(demo.verifying_key().as_bytes()).as_str())); + assert_eq!(genuine.outcome.as_deref(), Some("ok")); + // The forgery: other counts with the same sum, sealed with a key of the forger's. + let mut result = job["result"].clone(); + result.as_object_mut().unwrap().remove("receipt"); + result.as_object_mut().unwrap().remove("energy"); + result["counts"] = json!({ "0000000000000000": 4 }); + let other = SigningKey::from_bytes(&[9; 32]); + let forged = wai_wasmq::run::seal(&o, result, &other); + let v = wai_wasmq::verify(&forged, None, EngineKind::Interpreter).unwrap(); + assert!(v.signature, "a seal under any key is a valid seal: that is what a receipt is"); + assert_eq!(v.signer.as_deref(), Some(hex(other.verifying_key().as_bytes()).as_str())); + assert_ne!(v.signer, genuine.signer, "the forgery shows as another signer"); + // And with the bundle, the forged result is not the one a run gives. + assert_eq!(wai_wasmq::verify(&forged, Some(&o), EngineKind::Interpreter).unwrap().reproduced, Some(false)); +} + +// --------------------------------------------------------------------------- +// 4. A module is refused, never a panic, and is held to the feature set +// --------------------------------------------------------------------------- + +#[test] +fn a_malformed_module_is_refused_not_a_panic() { + let cases: [&[u8]; 2] = [ + // One type, one function of a type that does not exist, one body. + &[0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0, 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x03, 0x02, 0x01, 0x05, 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b], + // No type section at all. + &[0x00, 0x61, 0x73, 0x6d, 0x01, 0, 0, 0, 0x03, 0x02, 0x01, 0x05, 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b], + ]; + for bytes in cases { + let r = std::panic::catch_unwind(|| profile::check(bytes)); + assert_eq!(r.map(code_of).unwrap_or("panicked"), "W001", "{bytes:02x?}"); + } + // A function section that declares more functions than any reader holds, with one body. + let mut f = leb(1_000_001); + f.push(0); + let m = raw(&[unit_type(), section(3, &f), section(10, &[0x01, 0x02, 0x00, 0x0b])]); + let r = std::panic::catch_unwind(|| profile::check(&m)); + assert!(matches!(r.map(code_of), Ok("W001" | "W010")), "a function count past every reader's limit"); +} + +#[test] +fn a_body_that_does_not_end_or_is_not_in_the_feature_set_s_encoding_is_refused() { + let memory = section(5, &[0x01, 0x01, 0x01, 0x01]); + let one = section(3, &[0x01, 0x00]); + let cases: [(&str, Vec); 4] = [ + ("the positive control: i32.const 0, drop, end", raw(&[unit_type(), one.clone(), section(10, &[0x01, 0x05, 0x00, 0x41, 0x00, 0x1a, 0x0b])])), + ("a body with no end", raw(&[unit_type(), one.clone(), section(10, &[0x01, 0x04, 0x00, 0x41, 0x00, 0x1a])])), + ("i32.load with a memory index in its alignment", raw(&[unit_type(), one.clone(), memory.clone(), section(10, &[0x01, 0x09, 0x00, 0x41, 0x00, 0x28, 0x42, 0x00, 0x00, 0x1a, 0x0b])])), + ("memory.grow with an overlong memory index", raw(&[unit_type(), one.clone(), memory.clone(), section(10, &[0x01, 0x08, 0x00, 0x41, 0x00, 0x40, 0x80, 0x00, 0x1a, 0x0b])])), + ]; + for (i, (what, m)) in cases.iter().enumerate() { + let r = std::panic::catch_unwind(|| profile::check(m)).map(code_of).unwrap_or("panicked"); + assert_eq!(r, if i == 0 { "accepted" } else { "W001" }, "{what}"); + } +} + +#[test] +fn the_function_limit_is_one_a_module_can_reach() { + let at = |n: u64| { + let (f, c) = functions(n); + code_of(profile::check(&raw(&[unit_type(), f, c]))) + }; + assert_eq!(at(1 << 16), "accepted", "2^16 functions"); + assert_eq!(at((1 << 16) + 1), "W010", "one more"); +} + +// --------------------------------------------------------------------------- +// 5. A fault is the same fault on every engine +// --------------------------------------------------------------------------- + +/// The largest `n` below `past` for which `ok` holds, `ok` holding up to some `n` and no further. +fn largest(past: usize, ok: impl Fn(usize) -> bool) -> usize { + let (mut lo, mut hi) = (1, past); + assert!(ok(lo) && !ok(hi)); + while hi - lo > 1 { + let mid = (lo + hi) / 2; + if ok(mid) { lo = mid } else { hi = mid } + } + lo +} + +fn call_all(module: &[u8], name: &str, arg: u64) -> Vec<(Result, Trap>, i64)> { + engines(module) + .iter() + .map(|e| { + let mut i = e.instantiate().unwrap(); + i.set_fuel(1 << 40); + i.set_stack(0); + let r = i.call(name, &[arg]); + (r, i.fuel()) + }) + .collect() +} + +#[test] +fn an_indirect_call_where_the_stack_ends_faults_the_same_way_on_every_engine() { + // `$r` recurses `n` times and then calls through the table with an index outside it. + let m = wasm( + r#"(module (type $t (func (param i64) (result i64))) (table 1 funcref) + (func $r (param i64) (result i64) + local.get 0 i64.eqz + if (result i64) i64.const 0 i32.const 7 call_indirect (type $t) + else local.get 0 i64.const 1 i64.sub call $r end) + (func (export "f") (param i64) (result i64) local.get 0 call $r))"#, + ); + // The smallest recursion at which the interpreter stops for the stack. + let own = Interpreter::new(&m, Metering::Own).unwrap(); + let stops = |n: u64| { + let mut i = own.instantiate().unwrap(); + i.set_fuel(1 << 40); + i.set_stack(0); + i.call("f", &[n]) == Err(Trap::Stack) + }; + let (mut lo, mut hi) = (0u64, 1 << 24); + assert!(!stops(lo) && stops(hi)); + while hi - lo > 1 { + let mid = (lo + hi) / 2; + if stops(mid) { hi = mid } else { lo = mid } + } + for n in hi.saturating_sub(4)..=hi + 2 { + let got = call_all(&m, "f", n); + assert!(got.windows(2).all(|w| w[0] == w[1]), "recursing {n} times (the stack ends at {hi}): {:?}", got.iter().map(|g| &g.0).collect::>()); + } +} + +#[test] +fn every_way_an_indirect_call_traps_is_the_same_on_every_engine() { + let m = wasm( + r#"(module (type $t (func (param i64) (result i64))) (type $u (func (result i64))) + (table 4 funcref) (elem (i32.const 0) $ok $other) + (func $ok (type $t) local.get 0 i64.const 1 i64.add) + (func $other (type $u) i64.const 9) + (func (export "f") (param i64) (result i64) i64.const 5 local.get 0 i32.wrap_i64 call_indirect (type $t)))"#, + ); + for (index, want) in [(0u64, Ok(vec![6])), (1, Err(Trap::IndirectCallType)), (2, Err(Trap::IndirectCallNull)), (4, Err(Trap::TableOutOfBounds)), (u32::MAX as u64, Err(Trap::TableOutOfBounds))] { + let got = call_all(&m, "f", index); + assert!(got.windows(2).all(|w| w[0] == w[1]), "index {index}: {got:?}"); + assert_eq!(got[0].0, want, "index {index}"); + } +} + +#[test] +fn an_argument_is_read_as_a_value_of_its_parameter_s_type_on_every_engine() { + // Found by this crate's own sweep of the family, not by the review: the interpreter + // took a 64-bit argument for a 32-bit parameter whole, where the native engine takes + // its low 32 bits. No run reaches it (a host passes values of the type), but the + // engines are held to one answer for any call. + let m = wasm(r#"(module (func (export "i") (param i32) (result i32) local.get 0) (func (export "f") (param f32) (result f32) local.get 0))"#); + for name in ["i", "f"] { + let got = call_all(&m, name, (1 << 40) + 5); + assert!(got.windows(2).all(|w| w[0] == w[1]), "{name}: {got:?}"); + assert_eq!(got[0].0, Ok(vec![5]), "{name}"); + } +} + +// --------------------------------------------------------------------------- +// 6. The native engine holds every call the profile admits +// --------------------------------------------------------------------------- + +/// The reviewer's module: a function of forty parameters that recurses to the limit and +/// then holds tens of thousands of values across calls in two dozen locals. +fn rotating(steps: usize, depth: u64) -> Vec { + let mut body = format!("local.get 0 i64.const {depth} i64.lt_u if local.get 0 i64.const 1 i64.add "); + for p in 1..40 { + body.push_str(&format!("local.get {p} ")); + } + body.push_str("call $f drop end local.get 0 i64.const 99999999 i64.eq if "); + for k in 0..steps { + body.push_str(&format!("i32.const 0 i64.load local.set {} call $g local.get {} local.get 64 i64.add local.set 64 ", 40 + k % 24, 40 + (k + 1) % 24)); + } + body.push_str("end local.get 64 "); + for p in 0..40 { + body.push_str(&format!("local.get {p} i64.add ")); + } + let consts: String = (1..40).map(|p| format!("i64.const {p} ")).collect(); + wasm(&format!( + "(module (memory 1 1) (func $g i32.const 0 i32.const 0 i64.load i64.const 1 i64.add i64.store) (func $f (param {}) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 {consts} call $f))", + "i64 ".repeat(40), + "i64 ".repeat(25) + )) +} + +#[test] +fn a_module_the_profile_admits_runs_on_every_engine_at_the_depth_it_admits() { + // As the review gave it, 40,000 steps recursing 1023 times: a body past the profile's. + assert_eq!(profile::check(&rotating(40_000, 1023)).unwrap_err().code, "W010"); + // The most steps a body the profile admits holds, at the depth the stack admits, and + // one deeper: the same on every engine, and the first runs where the second faults. + let steps = largest(4000, |s| profile::check(&rotating(s, 1)).is_ok()); + assert!(steps > 500, "{steps} steps"); + let w = profile::check(&rotating(steps, 1)).unwrap().weights; + let depth = (u64::from(profile::MAX_STACK) - u64::from(w[2])) / u64::from(w[1]); + for (d, ok) in [(depth, true), (depth + 1, false)] { + let got = call_all(&rotating(steps, d), "f", 0); + assert!(got.windows(2).all(|w| w[0] == w[1]), "the engines differ at depth {d}: {:?}", got.iter().map(|g| format!("{:?}", g.0).chars().take(90).collect::()).collect::>()); + assert_eq!(got[0].0.is_ok(), ok, "depth {d}: {:?}", got[0].0); + } +} + +// --------------------------------------------------------------------------- +// 7. What is packed is what was given +// --------------------------------------------------------------------------- + +#[test] +fn an_input_a_double_cannot_hold_is_refused_not_changed() { + let program = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\ninput uint[64] mask;\nextern f(uint[64]) -> bit[8];\nqubit[8] q;\nbit[8] r;\nr = f(mask);\nh q;\nbit[8] b;\nb = measure q;\n"; + let module = wasm(IDENTITY); + for mask in [(1i64 << 53) + 1, i64::MAX - 100] { + let mut b = bundle(program, module.clone(), vec![]); + b.inputs.insert("mask".into(), mask); + match pack(&b) { + Err(e) => assert_eq!(e.code, "W017", "{e}"), + Ok(env) => assert_eq!(open(&env).map(|o| o.bundle.inputs).map_err(|e| e.to_string()), Ok(b.inputs.clone()), "pack wrote another input than the one given ({mask})"), + } + } + // The positive control: the largest input a double holds is packed as it is. + let mut b = bundle(program, module, vec![]); + b.inputs.insert("mask".into(), (1i64 << 53) - 1); + b.run.method = "stabilizer".into(); + assert_eq!(open(&pack(&b).unwrap()).unwrap().bundle, b); +} + +// --------------------------------------------------------------------------- +// 8. The profile runs what its text defines, and no more +// --------------------------------------------------------------------------- + +#[test] +fn a_program_outside_the_clifford_gates_is_refused() { + // A gate outside the group, and a gate that takes an angle, whatever the angle. + for gate in ["t q[0];", "rx(0.3) q[0];", "rz(pi / 2) q[0];", "ccx q[0], q[1], q[2];"] { + let program = SCALAR.replace("h q;", &format!("h q;\n{gate}")); + assert_eq!(code_of(pack(&bundle(&program, wasm(IDENTITY), vec![]))), "W016", "{gate}"); + } + assert_eq!(code_of(pack(&bundle(SCALAR, wasm(IDENTITY), vec![]))), "accepted"); +} + +#[test] +fn a_program_of_more_qubits_than_the_method_holds_is_refused_on_reading() { + let program = |n: u32| format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[{n}] q;\nbit[8] b;\nbit[8] r;\nh q[0];\nb[0] = measure q[0];\nr = f(b);\n"); + assert_eq!(code_of(pack(&bundle(&program(32_769), wasm(IDENTITY), vec![]))), "W016"); + // The largest the method holds is taken (its tableau is half a gibibyte: not run here). + assert!(open(&pack(&bundle(&program(32_768), wasm(IDENTITY), vec![])).unwrap()).is_ok()); + // And a program of many qubits runs: 3000 here. + let jobs = everywhere(&open(&pack(&bundle(&program(3000), wasm(IDENTITY), vec![])).unwrap()).unwrap()); + assert!(jobs.windows(2).all(|w| w[0] == w[1]) && jobs[0].contains("\"status\":\"done\""), "{}", &jobs[0][..jobs[0].len().min(200)]); +} + +#[test] +fn a_measurement_or_reset_takes_a_value_only_when_its_outcome_is_random() { + // §6.1, worked by hand for this program: `a` is put in superposition and reset (its + // outcome is random: the first value of the shot's stream); `q` is reset while still + // in |0> (determined: no value); then each of q's eight measurements is random, and + // takes the next value in order. `r` is `f(b)`, and `f` returns what it is given. + let program = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nqubit a;\nbit[8] b;\nbit[8] r;\nh a;\nreset a;\nreset q;\nh q;\nb = measure q;\nr = f(b);\n"; + let mut bun = bundle(program, wasm(IDENTITY), vec![]); + bun.run = Run::new(200, 42); + let o = open(&pack(&bun).unwrap()).unwrap(); + let mut want: std::collections::BTreeMap = Default::default(); + for shot in 0..200 { + let mut stream = wai_wasmq::run::Outcomes::of_shot(42, shot); + stream.next_reads_one(); // the reset of `a` + let b: Vec = (0..8).map(|_| stream.next_reads_one()).collect(); + // The outcome: r then b, each with its highest bit first. + let half: String = (0..8).rev().map(|i| if b[i] { '1' } else { '0' }).collect(); + *want.entry(format!("{half}{half}")).or_default() += 1; + } + let jobs = everywhere(&o); + assert!(jobs.windows(2).all(|w| w[0] == w[1])); + let job: Value = serde_json::from_str(&jobs[0]).unwrap(); + assert_eq!(job["result"]["counts"], json!(want)); + // The stream is the text's: SplitMix64 from the shot's seed XOR 0x51574d31, a value + // below 2^63 reading 1. + let mut state = wai_wasmq::run::shot_seed(42, 7) ^ 0x5157_4d31; + let mut stream = wai_wasmq::run::Outcomes::of_shot(42, 7); + for _ in 0..100 { + state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + assert_eq!(stream.next_reads_one(), (z ^ (z >> 31)) >> 63 == 0); + } +} + +// --------------------------------------------------------------------------- +// 11. What packs, runs; and each rule has its code +// --------------------------------------------------------------------------- + +#[test] +fn what_pack_accepts_every_engine_can_run() { + // `(func)` and an exported function of `calls` calls of it. + let module = |calls: usize| { + let mut body = vec![0x00]; + for _ in 0..calls { + body.extend_from_slice(&[0x10, 0x00]); + } + body.extend_from_slice(&[0x20, 0x00, 0x0b]); + let mut code = leb(2); + code.extend_from_slice(&[0x02, 0x00, 0x0b]); + code.extend(leb(body.len() as u64)); + code.extend_from_slice(&body); + let types = section(1, &[0x02, 0x60, 0x00, 0x00, 0x60, 0x01, 0x7e, 0x01, 0x7e]); + raw(&[types, section(3, &[0x02, 0x00, 0x01]), section(7, &[0x01, 0x01, b'f', 0x00, 0x01]), section(10, &code)]) + }; + // As the review gave it, 183,000 calls: a body past the profile's. + assert_eq!(pack(&bundle(SCALAR, module(183_000), vec![])).unwrap_err().code, "W010"); + // The most calls a body the profile admits holds: packed, and run the same everywhere. + let calls = largest(10_000, |n| profile::check(&module(n)).is_ok()); + assert!(calls > 8000, "{calls} calls"); + let env = pack(&bundle(SCALAR, module(calls), vec![])).unwrap(); + let jobs = everywhere(&open(&env).unwrap()); + assert!(jobs.windows(2).all(|w| w[0] == w[1]), "the engines differ"); + assert!(jobs[0].contains("\"status\":\"ok\""), "{}", &jobs[0][..jobs[0].len().min(200)]); +} + +#[test] +fn a_type_outside_the_profile_has_its_rule_s_code_with_or_without_an_instruction() { + for (wat, code) in [ + ("(module (func (param externref)))", "W007"), + ("(module (func (local funcref)))", "W007"), + ("(module (type (struct (field i32))))", "W007"), + ("(module (func (param v128)))", "W006"), + ("(module (func (local v128)))", "W006"), + ("(module (func (param i64)))", "accepted"), + ] { + assert_eq!(code_of(profile::check(&wasm(wat))), code, "{wat}"); + } +} + +#[test] +fn an_export_the_host_calls_has_exactly_its_type() { + let with = |export: &str| format!(r#"(module (memory (export "memory") 1 1) {export} (func (export "f") (param i64) (result i64) local.get 0))"#); + for (export, code) in [ + (r#"(func (export "wasmq-seed") (param i64))"#, "accepted"), + (r#"(func (export "wasmq-seed") (param i32))"#, "W012"), + (r#"(func (export "wasmq-seed") (param f64))"#, "W012"), + (r#"(func (export "init") (param i32))"#, "W012"), + (r#"(func (export "wasmq-prior") (param i32 i32 i64))"#, "W012"), + (r#"(func (export "wasmq-prior") (param i32 i32 i32))"#, "accepted"), + (r#"(func (export "cabi_realloc") (param i64 i64 i64 i64) (result i64) i64.const 0)"#, "W012"), + (r#"(func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)"#, "accepted"), + (r#"(func (export "cabi_post_f") (param i32 i32))"#, "W012"), + (r#"(func (export "cabi_post_f") (param i32))"#, "accepted"), + ] { + assert_eq!(code_of(profile::check(&wasm(&with(export)))), code, "{export}"); + } +} + +#[test] +fn a_contract_names_one_callout_for_each_extern_in_the_order_declared() { + let two = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nextern g(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nh q;\nb = measure q;\nr = f(b);\nr = g(r);\n"; + let module = wasm(r#"(module (func (export "f") (param i64) (result i64) local.get 0) (func (export "g") (param i64) (result i64) local.get 0))"#); + let callout = |name: &str, module: usize, export: &str| Callout { name: name.into(), module, export: export.into() }; + let with = |program: &str, callouts: Vec, modules: usize| { + let mut b = bundle(program, module.clone(), vec![]); + b.modules = (0..modules).map(|k| Module { name: format!("m{k}"), bytes: module.clone(), priors: vec![] }).collect(); + b.callouts = callouts; + code_of(open(&write_unchecked(&b, "stabilizer", 8).unwrap())) + }; + assert_eq!(with(two, vec![callout("f", 0, "f"), callout("g", 0, "g")], 1), "accepted"); + assert_eq!(with(two, vec![callout("g", 0, "g"), callout("f", 0, "f")], 1), "W015", "the other order"); + assert_eq!(with(SCALAR, vec![callout("f", 0, "f"), callout("f", 0, "g")], 1), "W015", "two callouts for one extern"); + assert_eq!(with(SCALAR, vec![callout("f", 0, "f"), callout("f", 7, "anything")], 1), "W015", "a second callout nothing checks"); + assert_eq!(with(SCALAR, vec![callout("f", 0, "f")], 2), "W015", "a module no callout names"); +} + +#[test] +fn a_module_with_priors_exports_what_the_host_calls_to_pass_them() { + let module = r#"(module (memory (export "memory") 1 1) (func (export "wasmq-prior") (param i32 i32 i32)) (func (export "f") (param i64) (result i64) local.get 0))"#; + assert_eq!(code_of(pack(&bundle(SCALAR, wasm(module), vec![("p".into(), vec![1, 2, 3])]))), "W015", "priors and no cabi_realloc"); + assert_eq!(code_of(pack(&bundle(SCALAR, wasm(module), vec![]))), "accepted", "no priors"); +} + +#[test] +fn a_result_holds_only_members_the_text_defines() { + let (_, done) = a_job(IDENTITY, 500); + let r = &done["result"]; + let keys = |v: &Value| v.as_object().map(|o| o.keys().cloned().collect::>()).unwrap_or_default(); + let mut k = keys(&r["modules"][0]); + k.sort(); + assert_eq!(k, ["blake3", "name"], "a module's members"); + assert_eq!(r["accuracy"], json!({ "class": "exact" })); + let trap = r#"(module (func (export "f") (param i64) (result i64) i64.const 256))"#; + let (_, fault) = a_job(trap, 500); + let mut k = keys(&fault["result"]["fault"]); + k.sort(); + assert_eq!(fault["result"]["fault"]["reason"], "bad_result"); + assert!(!k.contains(&"detail".to_string()), "a fault's members: {k:?}"); +} diff --git a/wai/wai-wasmq/tests/sweep.rs b/wai/wai-wasmq/tests/sweep.rs new file mode 100644 index 000000000..250b887d7 --- /dev/null +++ b/wai/wai-wasmq/tests/sweep.rs @@ -0,0 +1,768 @@ +//! What a sweep of the review's families found beyond the review (extensions/quantum-wasmq.md): +//! bundles a host could not hold or finish, which fuel does not meter; a job signed by any +//! key whose result this profile never writes; records that replay though no run made them. + +use serde_json::{Value, json}; +use wai_quantum::ed25519_dalek::SigningKey; +use wai_wasmq::envelope::{Bundle, Callout, Module, Run, open, pack}; +use wai_wasmq::run::{Call, EngineKind, replay, run}; + +fn wasm(wat: &str) -> Vec { + wat::parse_str(wat).unwrap() +} + +fn kinds() -> Vec { + wai_wasmq::engine::available().iter().map(|s| EngineKind::parse(s).unwrap()).collect() +} + +const IDENTITY: &str = r#"(module (func (export "f") (param i64) (result i64) local.get 0))"#; +const SPIN: &str = r#"(module (func (export "f") (param i64) (result i64) loop br 0 end i64.const 0))"#; + +fn one(program: String, module: Vec, shots: u64) -> Bundle { + Bundle { + program, + inputs: Default::default(), + modules: vec![Module { name: "m".into(), bytes: module, priors: vec![] }], + callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], + run: Run::new(shots, 1), + } +} + +/// A program of `qubits` qubits and `bits` bits in all, with `body` after a call. +fn program(qubits: u64, bits: u64, body: &str) -> String { + let rest = if bits > 1 { format!("bit[{}] c;\n", bits - 1) } else { String::new() }; + format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[{qubits}] q;\nbit[1] d;\n{rest}d = f(d);\n{body}") +} + +fn code(b: &Bundle) -> Option<&'static str> { + pack(b).err().map(|e| e.code) +} + +// --------------------------------------------------------------------------- +// What fuel does not meter: a host must be able to hold and finish what it opens +// --------------------------------------------------------------------------- + +/// `n` modules of `pages` pages each, one callout for each. +fn modules_of(n: usize, pages: u64) -> Bundle { + let mut p = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\n"); + let bytes = wasm(&format!(r#"(module (memory (export "memory") {pages} {pages}) (func (export "f") (param i64) (result i64) local.get 0))"#)); + let (mut modules, mut callouts) = (vec![], vec![]); + for i in 0..n { + p.push_str(&format!("extern f{i}(bit[1]) -> bit[1];\n")); + } + for i in 0..n { + p.push_str(&format!("c = f{i}(c);\n")); + modules.push(Module { name: format!("m{i}"), bytes: bytes.clone(), priors: vec![] }); + callouts.push(Callout { name: format!("f{i}"), module: i, export: "f".into() }); + } + Bundle { program: p, inputs: Default::default(), modules, callouts, run: Run::new(1, 1) } +} + +#[test] +fn the_code_among_a_bundle_s_modules_is_bounded() { + // The interpreter holds a module an instruction at a time, some fifty bytes each, and + // the native engine compiles each prepared body whole: thirty modules of the most bytes + // each, one envelope, would hold gigabytes on any engine (§3.2). + let of = |bytes: usize| { + let pad = format!("(data (i32.const 0) \"{}\")", "a".repeat(bytes)); + format!(r#"(module (memory 8 8) {pad} (func (export "f") (param i64) (result i64) local.get 0) (func (export "g") (param i64) (result i64) local.get 0))"#) + }; + let two = |bytes: usize| { + let mut b = Bundle { + program: "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nextern g(bit[1]) -> bit[1];\nqubit[1] q;\nbit[1] d;\nd = f(d);\nd = g(d);\n".into(), + inputs: Default::default(), + modules: vec![Module { name: "a".into(), bytes: wasm(&of(bytes)), priors: vec![] }, Module { name: "b".into(), bytes: wasm(&of(bytes)), priors: vec![] }], + callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }, Callout { name: "g".into(), module: 1, export: "g".into() }], + run: Run::new(1, 1), + }; + b.run.per_init = 1 << 24; + b + }; + let half = wai_wasmq::envelope::MAX_CODE_BYTES / 2; + assert_eq!(code(&two(half - 200)), None, "two modules of half the bytes each"); + assert_eq!(code(&two(half + 200)), Some("W010"), "two modules each within a module's bytes, past a bundle's together"); +} + +#[test] +fn the_modules_of_a_bundle_hold_no_more_memory_among_them_than_one_may() { + // Twenty modules of 256 MiB sealed `ok` on a native host, and on wasm32, whose address + // space cannot hold them, ended with no receipt: two outcomes for one envelope. + assert_eq!(code(&modules_of(20, 4096)), Some("W003"), "twenty modules of the most memory one may have"); + assert_eq!(code(&modules_of(2, 2049)), Some("W003"), "4098 pages among two"); + assert_eq!(code(&modules_of(2, 2048)), None, "4096 pages among two"); +} + +/// One bit of the program's register measured. +const ONE_MEASURED: &str = "c[0] = measure q[0];\n"; + +#[test] +fn a_bundle_whose_result_a_host_could_not_hold_is_refused() { + // A register of a million bits and 4096 shots: under 900 bytes, a job of 4 GB and 15 GB + // of memory on a native host, and on wasm32 the process aborts. + assert_eq!(code(&one(program(17, 1_000_000, ONE_MEASURED), wasm(IDENTITY), 4096)), Some("W017")); + // Shots times bits, at most 2^28. + assert_eq!(code(&one(program(1, 1 << 16, ONE_MEASURED), wasm(IDENTITY), 1 << 12)), None, "2^28 bits of results"); + assert_eq!(code(&one(program(1, (1 << 16) + 1, ONE_MEASURED), wasm(IDENTITY), 1 << 12)), Some("W017"), "one bit more in each shot"); + assert_eq!(code(&one(program(1, 1 << 20, ONE_MEASURED), wasm(IDENTITY), 1 << 8)), None, "2^28 bits, of the most bits a program holds"); + assert_eq!(code(&one(program(1, 1 << 20, ONE_MEASURED), wasm(IDENTITY), (1 << 8) + 1)), Some("W017"), "a shot more"); + // And a program holds at most 2^20 bits: past that its reader refuses it. + assert_eq!(code(&one(program(1, (1 << 20) + 1, ONE_MEASURED), wasm(IDENTITY), 1)), Some("W016"), "a bit more in a program"); +} + +#[test] +fn a_run_no_host_could_finish_is_refused_though_it_spends_no_fuel() { + // A measurement at the most qubits takes the host some milliseconds and no fuel: these + // would take a host far longer than anyone would wait, and it would seal nothing until then. + let measures = "h q;\nfor uint i in [0:99999] { c[0] = measure q[0]; }\n"; + assert_eq!(code(&one(program(32768, 2, measures), wasm(IDENTITY), 1 << 20)), Some("W017")); + // A fresh tableau at the most qubits, each shot. + assert_eq!(code(&one(program(32768, 2, ONE_MEASURED), wasm(IDENTITY), 1 << 20)), Some("W017"), "the tableaux"); + // A module's memory is made again each shot, and once for a run of one instance. + let mut b = one(program(1, 2, ""), wasm(r#"(module (memory (export "memory") 4096 4096) (func (export "f") (param i64) (result i64) local.get 0))"#), 1 << 20); + assert_eq!(code(&b), Some("W017"), "the memory of a million instances"); + b.run.state = "job".into(); + assert_eq!(code(&b), None, "the memory of one instance, for a million shots"); + b.run.state = "shot".into(); + b.run.shots = 1 << 10; + assert_eq!(code(&b), None, "the memory of a thousand instances"); + // Even once, a hundred thousand of them at the most qubits. + assert_eq!(code(&one(program(32768, 2, measures), wasm(IDENTITY), 1)), Some("W017"), "these measurements, once"); + // And programs of the same kinds at a size a host finishes pack. + let fewer = "h q;\nfor uint i in [0:1999] { c[0] = measure q[0]; }\n"; + assert_eq!(code(&one(program(32768, 2, fewer), wasm(IDENTITY), 1)), None, "two thousand measurements, once"); + assert_eq!(code(&one(program(32768, 2, ONE_MEASURED), wasm(IDENTITY), 1 << 11)), None, "these tableaux, two thousand times"); + // A call is the host's work too, whatever fuel it uses. + let calls = "for uint i in [0:1999] { d = f(d); }\nc[0] = measure q[0];\n"; + assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 20)), Some("W017"), "two thousand calls, a million times"); + assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 10)), None, "two thousand calls, a thousand times"); +} + +#[test] +fn memory_a_module_grows_is_paid_for_in_fuel_as_memory_it_starts_with_is_in_the_work() { + // Declared at no pages and grown to the most in `init`, each instance zeroes as much as + // one that starts with it: fuel pays for each page grown as the work counts each page + // a module starts with, so a run's budget bounds the one as the work bounds the other. + let grows = r#"(module (memory (export "memory") 0 4096) + (func (export "init") i32.const 4096 memory.grow drop) + (func (export "f") (param i64) (result i64) local.get 0))"#; + let starts = r#"(module (memory (export "memory") 4096 4096) (func (export "f") (param i64) (result i64) local.get 0))"#; + assert_eq!(code(&one(program(1, 2, ONE_MEASURED), wasm(starts), 1 << 20)), Some("W017"), "a million instances that start with the most memory"); + let o = open(&pack(&one(program(1, 2, ONE_MEASURED), wasm(grows), 4)).unwrap()).unwrap(); + let key = SigningKey::from_bytes(&[7; 32]); + for k in kinds() { + let (job, _) = run(&o, k, &key, false).unwrap(); + assert!(job["result"]["fuel"]["instances"].as_u64().unwrap() >= 4 * 4096 * wai_wasmq::meter::PAGE_FUEL, "{k:?}: {}", job["result"]["fuel"]); + } + // A million of them pack: their budget, which the work counts, runs out first. + let b = one(program(1, 2, ONE_MEASURED), wasm(grows), 1 << 20); + assert_eq!(code(&b), None); + assert!(u128::from(b.run.per_job) < 1 << 20 << 22, "the run's budget pays for fewer than a million instances' growth"); +} + +#[test] +fn a_run_s_fuel_is_bounded_with_its_work() { + // Fuel is the modules' work, and a host runs all of it: a budget of 2^53 is months of + // a loop that never ends, before the fuel fault is sealed. + let mut b = one(program(1, 2, ONE_MEASURED), wasm(SPIN), 1); + b.run.per_call = (1 << 53) - 1; + b.run.per_job = (1 << 53) - 1; + assert_eq!(code(&b), Some("W017"), "the most fuel a receipt can bind"); + // The budgets a run is given by default pack, and so does the most fuel the work allows. + assert_eq!(code(&one(program(1, 2, ONE_MEASURED), wasm(SPIN), 1)), None, "the default budgets"); + // Work of exactly the limit: gates of one each, so that the rest is whole units of fuel. + use wai_wasmq::envelope::{FUEL_WORK, MAX_HOST_WORK, host_work}; + let facts = [wai_wasmq::profile::check(&wasm(SPIN)).unwrap()]; + let with = |gates: u128| one(program(1, 2, &format!("{ONE_MEASURED}{}", "x q[0];\n".repeat(gates as usize))), wasm(SPIN), 1); + let work = |b: &Bundle| { + let (h, _) = wai_wasmq::envelope::read(&b.program, &b.inputs).unwrap(); + host_work(&h, &facts, b) + }; + let without_fuel = |b: &Bundle| work(&Bundle { run: Run { per_job: 0, ..b.run.clone() }, ..b.clone() }); + let mut most = with((MAX_HOST_WORK - without_fuel(&with(0))) % FUEL_WORK); + most.run.per_job = ((MAX_HOST_WORK - without_fuel(&most)) / FUEL_WORK) as u64; + most.run.per_call = most.run.per_job; + assert_eq!(work(&most), MAX_HOST_WORK, "work of exactly the limit"); + assert_eq!(code(&most), None, "the most fuel the work allows"); + most.run.per_job += 1; + assert_eq!(code(&most), Some("W017"), "one more"); +} + +#[test] +fn a_callout_s_work_counts_every_call_into_its_module() { + // A list argument is a call of cabi_realloc, the export is one, and cabi_post another: a + // callout of three list arguments with a post call is five calls of the host's. + let m = wasm(r#"(module (memory (export "memory") 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "f") (param i32 i32 i32 i32 i32 i32) (result i32) i32.const 2048) + (func (export "cabi_post_f") (param i32)))"#); + let src = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[65], bit[65], bit[65]) -> bit[65];\nqubit[1] q;\nbit[65] a;\nbit[65] r;\nr = f(a, a, a);\n"; + let b = one(src.into(), m, 1); + let (h, _) = wai_wasmq::envelope::read(&b.program, &b.inputs).unwrap(); + let facts = [wai_wasmq::profile::check(&b.modules[0].bytes).unwrap()]; + let one_call = wai_wasmq::envelope::host_work(&h, &facts, &b); + let mut twice = b.clone(); + twice.program = src.replace("r = f(a, a, a);", "r = f(a, a, a);\nr = f(a, a, a);"); + let (h2, _) = wai_wasmq::envelope::read(&twice.program, &twice.inputs).unwrap(); + let call = wai_wasmq::envelope::host_work(&h2, &facts, &twice) - one_call; + assert_eq!(call, 5 * wai_wasmq::envelope::CALL_WORK + 4 * 65 * wai_wasmq::envelope::BIT_WORK + 1, "five calls of the host's and 260 bits, and the step"); + // A prior is two: cabi_realloc and wasmq-prior. + let p = wasm(TAKES_PRIORS); + let mut with = one(program(1, 1, ""), p.clone(), 1); + let facts = [wai_wasmq::profile::check(&p).unwrap()]; + let (h, _) = wai_wasmq::envelope::read(&with.program, &with.inputs).unwrap(); + let none = wai_wasmq::envelope::host_work(&h, &facts, &with); + with.modules[0].priors.push(("p".into(), vec![0; 64])); + assert_eq!(wai_wasmq::envelope::host_work(&h, &facts, &with) - none, 2 * wai_wasmq::envelope::CALL_WORK); +} + +#[test] +fn the_work_a_run_asks_of_its_host_is_the_text_s_sum() { + use wai_wasmq::envelope::{BIT_WORK, CALL_WORK, CODE_WORK, FUEL_WORK, MEASURE_WORK}; + // §4.8, by hand: 64 qubits take r = 2 words a qubit. For a shot: a tableau of + // 2 * 64 * 2; the call, one of the host's with its two bits and its step; a gate of 2, + // twice; the measurement, 32 * 64 * 2; the jump, 1. For an instance: making it, one + // call, its module's bytes, and three pages of 8192. For the run: its module's bytes, + // compiled, and the fuel it may be given. + let src = program(64, 2, "x q[0];\nc[0] = measure q[0];\nif (c[0]) x q[1];\n"); + let m = wasm(r#"(module (memory (export "memory") 3 9) (func (export "f") (param i64) (result i64) local.get 0))"#); + let (h, _) = wai_wasmq::envelope::read(&src, &Default::default()).unwrap(); + let work = |b: &Bundle| wai_wasmq::envelope::host_work(&h, &[wai_wasmq::profile::check(&b.modules[0].bytes).unwrap()], b); + let shot = 2 * 64 * 2 + (CALL_WORK + 2 * BIT_WORK + 1) + 2 + MEASURE_WORK * 64 * 2 + 1 + 2; + let len = m.len() as u128; + let instance = CALL_WORK + len + 3 * 8192; + let mut b = one(src.clone(), m, 5); + let run = CODE_WORK * len + FUEL_WORK * u128::from(b.run.per_job); + assert_eq!(work(&b), 5 * shot + 5 * instance + run, "an instance for each shot"); + b.run.state = "job".into(); + assert_eq!(work(&b), 5 * shot + instance + run, "one instance for the run"); + // A seed, an init and a prior are four more calls: a prior is cabi_realloc and wasmq-prior. + let m = wasm(r#"(module (memory (export "memory") 3 9) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "wasmq-prior") (param i32 i32 i32)) + (func (export "wasmq-seed") (param i64)) + (func (export "init")) + (func (export "f") (param i64) (result i64) local.get 0))"#); + let len = m.len() as u128; + let mut b = one(src, m, 5); + b.modules[0].priors.push(("p".into(), vec![0; 17])); + assert_eq!(work(&b), 5 * shot + 5 * (5 * CALL_WORK + len + 3 * 8192) + CODE_WORK * len + FUEL_WORK * u128::from(b.run.per_job)); +} + +#[test] +fn the_work_limit_admits_its_largest_and_refuses_one_more() { + // A million shots of an instance of 480 pages, and of gates of one word each, as many + // as the work leaves room for. + let m = wasm(r#"(module (memory (export "memory") 480 480) (func (export "f") (param i64) (result i64) local.get 0))"#); + let with = |gates: u64| one(program(1, 2, &format!("c[0] = measure q[0];\nfor uint i in [0:{}] {{ x q[0]; }}\n", gates - 1)), m.clone(), 1 << 20); + let facts = [wai_wasmq::profile::check(&m).unwrap()]; + let work = |g: u64| { + let b = with(g); + let (h, _) = wai_wasmq::envelope::read(&b.program, &b.inputs).unwrap(); + wai_wasmq::envelope::host_work(&h, &facts, &b) + }; + let per_gate = work(2) - work(1); + assert_eq!(per_gate, 1 << 20, "a gate of one word, in each of a million shots"); + let most = 1 + ((wai_wasmq::envelope::MAX_HOST_WORK - work(1)) / per_gate) as u64; + assert!(most > 1000 && work(most) <= wai_wasmq::envelope::MAX_HOST_WORK && work(most + 1) > wai_wasmq::envelope::MAX_HOST_WORK); + assert_eq!(code(&with(most)), None, "the most the work admits"); + assert_eq!(code(&with(most + 1)), Some("W017"), "a gate more in each shot"); +} + +#[test] +fn the_steps_of_a_program_are_bounded_at_their_limit() { + // A call and its loop's body: a step each. The program's first call makes one more. + let of = |calls: u64| one(program(1, 2, &format!("for uint i in [0:{}] {{ d = f(d); }}\nc[0] = measure q[0];\n", calls - 1)), wasm(IDENTITY), 1); + let room = (1 << 21) - 2; + assert_eq!(code(&of(room)), None, "2^21 steps"); + assert_eq!(code(&of(room + 1)), Some("W016"), "one more"); + // Steps of jumps alone past the bound: an `if` and an `else` with nothing in them are + // two jumps, which no step of a gate, a measurement or a call counts as it is made. + let jumps = |calls: u64| one(program(1, 2, &format!("for uint i in [0:{}] {{ d = f(d); }}\nc[0] = measure q[0];\nif (d == 1) {{ }} else {{ }}\n", calls - 1)), wasm(IDENTITY), 1); + let (h, _) = wai_wasmq::envelope::read(&jumps(room - 2).program, &Default::default()).unwrap(); + assert_eq!(h.steps.len(), 1 << 21, "the empty branches are two steps"); + let past = jumps(room - 1); + let e = pack(&past).unwrap_err(); + assert!(e.code == "W016" && e.message.contains("steps; it is held to"), "{e}"); +} + +/// A module that takes priors, so that a long prior is an envelope's only defect. +const TAKES_PRIORS: &str = r#"(module (memory (export "memory") 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "wasmq-prior") (param i32 i32 i32)) + (func (export "f") (param i64) (result i64) local.get 0))"#; + +fn with_prior(bytes: usize) -> Bundle { + let mut b = one(program(1, 1, ""), wasm(TAKES_PRIORS), 1); + b.modules[0].priors.push(("long".into(), vec![0; bytes])); + b +} + +#[test] +fn an_envelope_past_what_a_host_must_hold_is_refused() { + // A reader holds an envelope's bytes and its frames at once, so what one host opens + // another must be able to. + // Frames of the whole limit are refused by the check itself, before they are written. + let whole = with_prior(1 << 27); + assert_eq!(wai_wasmq::envelope::check(&whole).err().map(|e| e.code), Some("W013"), "frames of the whole limit"); + assert_eq!(code(&whole), Some("W013")); + // Frames just within it, and with the contract and the manifest past it: the reader's. + let rest = with_prior(0); + let others = rest.program.len() + rest.modules[0].bytes.len(); + let just_within = with_prior((1 << 27) - 1 - others); + assert!(wai_wasmq::envelope::check(&just_within).is_ok(), "the frames alone fit"); + assert_eq!(code(&just_within), Some("W013"), "the envelope they make does not"); + let env = pack(&with_prior((1 << 27) - (1 << 16))).unwrap(); + assert!(env.len() <= 1 << 27); + assert!(open(&env).is_ok(), "an envelope within the limit"); +} + +/// A module of the most memory and the most code a bundle holds: the exports a prior +/// needs, `f`, and functions of one-byte instructions up to the bundle's bytes of module, +/// which the interpreter holds an instruction at a time. +fn the_most_code() -> Vec { + let head = r#"(memory (export "memory") 4096 4096) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "wasmq-prior") (param i32 i32 i32)) + (func (export "f") (param i64) (result i64) local.get 0)"#; + let filler = format!("(func i32.const 0 {}drop)", "i32.eqz ".repeat(wai_wasmq::profile::MAX_BODY_BYTES as usize - 8)); + let mut fillers = 0; + loop { + let m = wasm(&format!("(module {head} {})", filler.repeat(fillers + 1))); + if m.len() > wai_wasmq::envelope::MAX_CODE_BYTES { + let m = wasm(&format!("(module {head} {})", filler.repeat(fillers))); + assert!(m.len() > wai_wasmq::envelope::MAX_CODE_BYTES - (20 << 10), "{} bytes", m.len()); + return m; + } + fillers += 1; + } +} + +/// A bundle at every limit a run holds in memory at once: the most memory and code, a +/// prior of the rest an envelope holds, the most qubits and bits, and a determined +/// measurement; with `calls` calls that take and give two bits each. +fn the_largest(calls: u64) -> wai_wasmq::Opened { + let src = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[2]) -> bit[2];\nqubit[32768] q;\nbit[2] d;\nbit[{}] c;\nd = f(d);\nh q[0];\nc[0] = measure q[0];\nreset q[0];\nfor uint i in [0:{}] {{ d = f(d); }}\n", + (1 << 20) - 2, + calls - 1 + ); + let m = the_most_code(); + let room = wai_wasmq::envelope::MAX_ENVELOPE_BYTES - m.len() - src.len() - (8 << 10); + let mut b = one(src, m, 2); + b.modules[0].priors.push(("long".into(), vec![1; room])); + // Fuel counts a prior's bytes: the budget lets it through, so that the run goes on to + // hold everything else at once, as it would not if it stopped at the prior. + b.run.per_init = 1 << 28; + open(&pack(&b).unwrap()).unwrap() +} + +#[test] +fn the_largest_bundle_the_limits_admit_runs_on_every_engine() { + let o = the_largest(1); + let key = SigningKey::from_bytes(&[7; 32]); + let jobs: Vec = kinds().into_iter().map(|k| run(&o, k, &key, false).unwrap().0).collect(); + assert_eq!(jobs[0]["result"]["status"], "ok", "{}", jobs[0]["result"]["fault"]); + assert!(jobs.iter().all(|j| j == &jobs[0]), "the same job on every engine"); +} + +#[test] +fn the_largest_bundle_with_the_most_steps_fits_a_32_bit_host() { + // And with the most steps and the most bits of calls as well, on the interpreter, which + // is the engine wasm32 has. + let calls = (1u64 << 21) - 8; + assert!(4 * (calls + 1) <= wai_wasmq::envelope::MAX_CALLOUT_BITS && 4 * (calls + 1) > wai_wasmq::envelope::MAX_CALLOUT_BITS - 64); + let o = the_largest(calls); + let (job, _) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), false).unwrap(); + assert_eq!(job["result"]["status"], "ok", "{}", job["result"]["fault"]); +} + +#[test] +fn the_most_result_a_run_may_have_runs_on_every_engine() { + // 256 shots of the most bits a program holds, nine of them random: 2^28 bits of results, + // in hundreds of outcomes of a million bits each. + let measures: String = (0..9).map(|k| format!("c[{k}] = measure q[{k}];\n")).collect(); + let b = one(program(9, 1 << 20, &format!("h q;\n{measures}")), wasm(IDENTITY), 1 << 8); + let o = open(&pack(&b).unwrap()).unwrap(); + let key = SigningKey::from_bytes(&[7; 32]); + let jobs: Vec = kinds().into_iter().map(|k| run(&o, k, &key, false).unwrap().0).collect(); + assert!(jobs[0]["result"]["counts"].as_object().unwrap().len() > 100); + assert!(jobs.iter().all(|j| j == &jobs[0]), "the same job on every engine"); +} + +#[test] +fn every_jump_of_a_program_goes_forward_so_a_run_ends() { + // Fuel meters no step of the program, so a run's end rests on this: the reader unrolls + // every loop, a `while` included, and a step only ever jumps ahead. + let src = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[1] q;\nbit[1] c;\nc[0] = measure q[0];\n@wai.max_iterations 5\nwhile (c == 0) { x q[0]; c = f(c); c[0] = measure q[0]; }\nfor uint i in [0:3] { if (c == 1) x q[0]; else h q[0]; }\n"; + let h = wai_quantum::qasm::read_callouts(src, &Default::default()).unwrap(); + let mut jumps = 0; + for (pc, s) in h.steps.iter().enumerate() { + if let wai_quantum::qasm::Step::Instr(wai_quantum::qasm::Instr::JumpUnless(_, to) | wai_quantum::qasm::Instr::Jump(to)) = s { + jumps += 1; + assert!(*to > pc, "step {pc} jumps back to {to}"); + } + } + assert!(jumps > 0, "this test can fail: the program has jumps"); +} + +// --------------------------------------------------------------------------- +// A job sealed by any key: its result is one this profile writes, or verify says not +// --------------------------------------------------------------------------- + +fn a_job(module: &str, per_call: u64) -> (wai_wasmq::Opened, Value) { + let mut b = one(program(3, 3, "h q;\nc[0] = measure q[0];\nc[1] = measure q[1];\n"), wasm(module), 8); + b.run.per_call = per_call; + let o = open(&pack(&b).unwrap()).unwrap(); + let (body, _) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), false).unwrap(); + (o, body) +} + +/// The job as it would be sealed again under another key, its body agreeing with its result. +fn sealed_again(job: &Value) -> Value { + let mut result = job["result"].clone(); + if let Some(o) = result.as_object_mut() { + o.remove("receipt"); + } + let receipt = wai_quantum::engine::receipt::seal(&SigningKey::from_bytes(&[9; 32]), &job["request"], &result); + if result.is_object() { + result["receipt"] = receipt; + } + let status = if result["status"] == "ok" { "done" } else { "fault" }; + json!({ "status": status, "method": result["method"], "request": job["request"], "result": result }) +} + +/// Each edit of an `ok` job's result or request, and of a faulted one's, that makes it one +/// this profile never writes. +type Edit = (&'static str, fn(&mut Value)); + +const EDITS_OF_OK: &[Edit] = &[ + ("another profile", |j| j["result"]["profile"] = json!("wai.quantum.wasmq/2")), + ("a method the contract does not give", |j| j["result"]["method"] = json!("statevector")), + ("shots the contract does not give", |j| { + j["result"]["shots"] = json!(9); + j["result"]["shots_run"] = json!(9); + let k = j["result"]["counts"].as_object().unwrap().keys().next().unwrap().clone(); + let n = j["result"]["counts"][&k].as_u64().unwrap(); + j["result"]["counts"][&k] = json!(n + 1); + }), + ("a seed the contract does not give", |j| j["result"]["seed"] = json!(2)), + ("more qubits than a program may have", |j| j["result"]["qubits"] = json!(40000)), + ("qubits that are not a number", |j| j["result"]["qubits"] = json!("three")), + ("a count that is not a number", |j| j["result"]["counts"]["1"] = json!("many")), + ("a count of nothing", |j| j["result"]["counts"]["1"] = json!(0)), + ("an outcome that is not bits", |j| { + let c = j["result"]["counts"].as_object_mut().unwrap(); + let (k, n) = c.iter().next().map(|(k, n)| (k.clone(), n.clone())).unwrap(); + c.remove(&k); + c.insert("2".repeat(k.len()), n); + }), + ("outcomes of two widths", |j| { + let c = j["result"]["counts"].as_object_mut().unwrap(); + let (k, n) = c.iter().next().map(|(k, n)| (k.clone(), n.clone())).unwrap(); + c.remove(&k); + c.insert(format!("{k}0"), n); + }), + ("counts that do not sum to the shots run", |j| { + let k = j["result"]["counts"].as_object().unwrap().keys().next().unwrap().clone(); + j["result"]["counts"] = json!({ k: 7 }); + }), + ("counts that overflow to the shots run", |j| { + j["result"]["counts"] = json!({ "0": u64::MAX, "1": 9 }); + }), + ("a member the text does not define", |j| j["result"]["note"] = json!("checked by a person of standing")), + ("no transcript", |j| { + j["result"].as_object_mut().unwrap().remove("transcript_blake3"); + }), + ("no accuracy", |j| { + j["result"].as_object_mut().unwrap().remove("accuracy"); + }), + ("no energy", |j| { + j["result"].as_object_mut().unwrap().remove("energy"); + }), + ("no count of the callouts", |j| { + j["result"].as_object_mut().unwrap().remove("callouts"); + }), + ("another accuracy", |j| j["result"]["accuracy"] = json!({ "class": "approximate" })), + ("an energy reading", |j| j["result"]["energy"] = json!({ "joules": 0.001 })), + ("a transcript that is no digest", |j| j["result"]["transcript_blake3"] = json!("abc")), + ("callouts that are not the calls counted", |j| { + let n = j["result"]["callouts"].as_u64().unwrap(); + j["result"]["callouts"] = json!(n + 1); + }), + ("fuel used that is not the fuel counted", |j| { + let n = j["result"]["fuel"]["used"].as_u64().unwrap(); + j["result"]["fuel"]["used"] = json!(n + 1); + }), + ("instances' fuel that is not the fuel counted", |j| { + let n = j["result"]["fuel"]["instances"].as_u64().unwrap(); + j["result"]["fuel"]["instances"] = json!(n + 1); + }), + ("a budget the contract does not give", |j| j["result"]["fuel"]["budget"]["per_call"] = json!(1)), + ("a member of the fuel the text does not define", |j| j["result"]["fuel"]["note"] = json!(1)), + ("fuel for a callout the contract does not name", |j| j["result"]["fuel"]["callouts"]["g"] = json!({ "calls": 1, "fuel": 0, "max": 0 })), + ("a call counted that was not", |j| { + let n = j["result"]["fuel"]["callouts"]["f"]["calls"].as_u64().unwrap(); + j["result"]["fuel"]["callouts"]["f"]["calls"] = json!(n + 1); + }), + ("a largest call past the callout's fuel", |j| { + let n = j["result"]["fuel"]["callouts"]["f"]["fuel"].as_u64().unwrap(); + j["result"]["fuel"]["callouts"]["f"]["max"] = json!(n + 1); + }), + ("a member of a callout's fuel the text does not define", |j| j["result"]["fuel"]["callouts"]["f"]["note"] = json!(1)), + ("a member of a module the text does not define", |j| j["result"]["modules"][0]["note"] = json!(1)), + ("an ok run that names a fault", |j| j["result"]["fault"] = json!({ "reason": "unreachable", "shot": 0, "phase": "call", "module": "m", "fuel": 0, "callout": "f" })), + ("a member of the request the text does not define", |j| j["request"]["note"] = json!(1)), + ("an envelope that is no digest", |j| j["request"]["envelope_blake3"] = json!("abc")), +]; + +const EDITS_OF_FAULT: &[Edit] = &[ + ("a reason §6.2 does not name", |j| j["result"]["fault"]["reason"] = json!("cosmic_ray")), + ("a fault in a shot other than the one that stopped", |j| j["result"]["fault"]["shot"] = json!(5)), + ("a phase §6.2 does not name", |j| j["result"]["fault"]["phase"] = json!("nowhere")), + ("a module the contract does not name", |j| j["result"]["fault"]["module"] = json!("nobody")), + ("a callout the contract does not name", |j| j["result"]["fault"]["callout"] = json!("nobody")), + ("a call's fault with no callout", |j| { + j["result"]["fault"].as_object_mut().unwrap().remove("callout"); + }), + ("a callout named where the phase is not a call", |j| j["result"]["fault"]["phase"] = json!("init")), + ("a budget of no one", |j| j["result"]["fault"]["budget"] = json!("nobody's")), + ("a fuel fault with no budget", |j| { + j["result"]["fault"].as_object_mut().unwrap().remove("budget"); + }), + ("a budget on a fault of another reason", |j| j["result"]["fault"]["reason"] = json!("unreachable")), + ("a fault's fuel that is not a number", |j| j["result"]["fault"]["fuel"] = json!("some")), + ("a member of a fault the text does not define", |j| j["result"]["fault"]["detail"] = json!("free text under the signature")), + ("a fault after every shot finished", |j| { + // The fault's shot follows them, so that nothing but this is out of place. + j["result"]["shots_run"] = json!(8); + j["result"]["counts"] = json!({ "0": 8 }); + j["result"]["fault"]["shot"] = json!(8); + }), +]; + +/// Each edit of an `ok` job's contract that makes it one this profile never writes, with +/// the result agreeing with it wherever the result repeats it (§2.2, §6.4). +const EDITS_OF_CONTRACT: &[Edit] = &[ + ("a method this version does not have, in the contract and the result", |j| { + j["request"]["wasmq"]["run"]["method"] = json!("statevector"); + j["result"]["method"] = json!("statevector"); + }), + ("no shots, in the contract and the result", |j| { + j["request"]["wasmq"]["run"]["shots"] = json!(0); + j["result"]["shots"] = json!(0); + j["result"]["shots_run"] = json!(0); + j["result"]["counts"] = json!({}); + }), + ("more shots than a run takes, in the contract and the result", |j| { + j["request"]["wasmq"]["run"]["shots"] = json!((1u64 << 20) + 1); + j["result"]["shots"] = json!((1u64 << 20) + 1); + }), + ("a seed that is no number, in the contract and the result", |j| { + j["request"]["wasmq"]["run"]["seed"] = json!(null); + j["result"]["seed"] = json!(null); + }), + ("a contract of another profile", |j| j["request"]["wasmq"]["profile"] = json!("wai.quantum.wasmq/2")), + ("a contract with a member the text does not define", |j| j["request"]["wasmq"]["note"] = json!(1)), + ("a contract with no program", |j| { + j["request"]["wasmq"].as_object_mut().unwrap().remove("program"); + }), + ("a program that is no digest", |j| j["request"]["wasmq"]["program"]["blake3"] = json!("abc")), + ("a program's inputs that are not integers", |j| j["request"]["wasmq"]["program"]["inputs"] = json!({ "n": "two" })), + ("instances of no kind", |j| j["request"]["wasmq"]["run"]["state"] = json!("forever")), + ("a run with a member the text does not define", |j| j["request"]["wasmq"]["run"]["note"] = json!(1)), + ("a budget that is no number, in the contract and the result", |j| { + j["request"]["wasmq"]["run"]["fuel"]["per_instance"] = json!("lots"); + j["result"]["fuel"]["budget"]["per_instance"] = json!("lots"); + }), + ("a module that is no digest, in the contract and the result", |j| { + j["request"]["wasmq"]["modules"][0]["blake3"] = json!(null); + j["result"]["modules"][0]["blake3"] = json!(null); + }), + ("a module of no length", |j| j["request"]["wasmq"]["modules"][0]["bytes"] = json!(0)), + ("a callout of a module the contract does not hold", |j| j["request"]["wasmq"]["callouts"][0]["module"] = json!(5)), + ("a callout of no export", |j| j["request"]["wasmq"]["callouts"][0]["export"] = json!(7)), + ("two callouts for one extern", |j| { + let c = j["request"]["wasmq"]["callouts"][0].clone(); + j["request"]["wasmq"]["callouts"].as_array_mut().unwrap().push(c); + }), + ("instances' fuel past what the instances could be given", |j| { + // Two lifecycle calls a module (its seed and its init) and one for each prior, for + // each instance: one for each shot, and one more for a shot that stopped. + let per = j["request"]["wasmq"]["run"]["fuel"]["per_instance"].as_u64().unwrap(); + let shots = j["result"]["shots"].as_u64().unwrap(); + let over = per * 2 * (shots + 1) + 1; + j["result"]["fuel"]["instances"] = json!(over); + let used = j["result"]["fuel"]["used"].as_u64().unwrap(); + j["result"]["fuel"]["used"] = json!(used + over); + j["request"]["wasmq"]["run"]["fuel"]["per_job"] = json!(used + over); + j["result"]["fuel"]["budget"]["per_job"] = json!(used + over); + }), + ("a callout's fuel past its calls times its largest", |j| { + let f = &j["result"]["fuel"]["callouts"]["f"]; + let (calls, max, fuel) = (f["calls"].as_u64().unwrap(), f["max"].as_u64().unwrap(), f["fuel"].as_u64().unwrap()); + let more = calls * max + 1 - fuel; + j["result"]["fuel"]["callouts"]["f"]["fuel"] = json!(calls * max + 1); + let used = j["result"]["fuel"]["used"].as_u64().unwrap(); + j["result"]["fuel"]["used"] = json!(used + more); + }), + ("fuel used past the run's budget, in the contract and the result", |j| { + let used = j["result"]["fuel"]["used"].as_u64().unwrap(); + j["request"]["wasmq"]["run"]["fuel"]["per_job"] = json!(used - 1); + j["result"]["fuel"]["budget"]["per_job"] = json!(used - 1); + }), + ("a call past the call's budget, in the contract and the result", |j| { + let max = j["result"]["fuel"]["callouts"]["f"]["max"].as_u64().unwrap(); + j["request"]["wasmq"]["run"]["fuel"]["per_call"] = json!(max - 1); + j["result"]["fuel"]["budget"]["per_call"] = json!(max - 1); + }), + ("callouts' fuel that is no object, and no calls", |j| { + j["result"]["callouts"] = json!(0); + j["result"]["fuel"]["callouts"] = json!(0); + j["result"]["fuel"]["used"] = j["result"]["fuel"]["instances"].clone(); + }), +]; + +/// Edits of a fault job that make it one this profile never writes. +const EDITS_OF_FAULT_FUEL: &[Edit] = &[("a fault's fuel past the budget of its phase", |j| { + // One more than the call's budget, and one more used, counted to the instances: nothing + // but the fault's own fuel is out of place. + let per = j["request"]["wasmq"]["run"]["fuel"]["per_call"].as_u64().unwrap(); + j["result"]["fault"]["fuel"] = json!(per + 1); + for at in ["used", "instances"] { + let n = j["result"]["fuel"][at].as_u64().unwrap(); + j["result"]["fuel"][at] = json!(n + 1); + } +})]; + +/// Edits of a sealed job's receipt, after it was sealed. +const EDITS_OF_RECEIPT: &[Edit] = &[ + ("a receipt of another algorithm", |j| j["result"]["receipt"]["alg"] = json!("none")), + ("a receipt that names no algorithm", |j| { + j["result"]["receipt"].as_object_mut().unwrap().remove("alg"); + }), + ("a receipt of the first form", |j| { + j["result"]["receipt"].as_object_mut().unwrap().remove("canon"); + }), + ("a key that is not hexadecimal", |j| j["result"]["receipt"]["key"] = json!("€€")), + ("a key in capitals", |j| { + let k = j["result"]["receipt"]["key"].as_str().unwrap().to_uppercase(); + j["result"]["receipt"]["key"] = json!(k); + }), + ("a signature that is not hexadecimal", |j| j["result"]["receipt"]["signature"] = json!("€".repeat(64))), +]; + +#[test] +fn a_job_whose_contract_or_receipt_this_profile_never_writes_is_not_one_of_its_jobs() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + let (_, ok) = a_job(IDENTITY, 500); + let (_, fault) = a_job(SPIN, 500); + let mut let_through = Vec::new(); + let mut judge = |name: &'static str, job: &Value| { + let v = catch_unwind(AssertUnwindSafe(|| wai_wasmq::verify(job, None, EngineKind::Interpreter))).unwrap_or_else(|_| panic!("{name}: verify panicked")); + if v.as_ref().is_ok_and(|v| v.ok()) { + let_through.push(name); + } + }; + for (job, edits) in [(&ok, EDITS_OF_CONTRACT), (&fault, EDITS_OF_FAULT_FUEL)] { + for (name, edit) in edits { + let mut edited = job.clone(); + edit(&mut edited); + assert_ne!(&edited, job, "{name} changes the job"); + judge(name, &sealed_again(&edited)); + } + } + let sealed = sealed_again(&ok); + assert!(wai_wasmq::verify(&sealed, None, EngineKind::Interpreter).unwrap().ok(), "the positive control"); + for (name, edit) in EDITS_OF_RECEIPT { + let mut edited = sealed.clone(); + edit(&mut edited); + judge(name, &edited); + } + assert!(let_through.is_empty(), "verify calls these jobs of this profile: {let_through:#?}"); +} + +#[test] +fn a_job_whose_result_this_profile_never_writes_is_not_one_of_its_jobs_under_any_key() { + let (_, ok) = a_job(IDENTITY, 500); + let (_, fault) = a_job(SPIN, 500); + assert_eq!(fault["result"]["fault"]["reason"], "fuel_exhausted"); + let mut let_through = Vec::new(); + for (job, edits) in [(&ok, EDITS_OF_OK), (&fault, EDITS_OF_FAULT)] { + // The positive control: sealed again unchanged, it is a job of this profile. + let v = wai_wasmq::verify(&sealed_again(job), None, EngineKind::Interpreter).unwrap(); + assert!(v.signature && v.well_formed.is_empty(), "sealed again unchanged: {:?}", v.well_formed); + for (name, edit) in edits { + let mut edited = job.clone(); + edit(&mut edited); + assert_ne!(&edited, job, "{name} changes the job"); + let v = wai_wasmq::verify(&sealed_again(&edited), None, EngineKind::Interpreter).unwrap(); + assert!(v.signature, "{name}: a seal under any key is a valid seal, so this is the form's to catch"); + if v.well_formed.is_empty() { + let_through.push(*name); + } + } + } + assert!(let_through.is_empty(), "verify calls these jobs of this profile: {let_through:#?}"); +} + +// --------------------------------------------------------------------------- +// A record replays as one a run could have made +// --------------------------------------------------------------------------- + +fn a_record() -> (wai_wasmq::Opened, Vec) { + let o = open(&pack(&one(program(3, 1, "h q;\n"), wasm(IDENTITY), 8)).unwrap()).unwrap(); + let (_, log) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), true).unwrap(); + (o, log.unwrap()) +} + +#[test] +fn a_record_of_a_shot_the_run_does_not_have_or_out_of_order_does_not_replay() { + let (o, log) = a_record(); + assert_eq!(log.len(), 8); + assert!(replay(&o, &log, EngineKind::Interpreter).unwrap().is_empty(), "the positive control"); + let mut past = log.clone(); + past[7].shot = 8; + assert!(!replay(&o, &past, EngineKind::Interpreter).unwrap().is_empty(), "a shot past the run's eight"); + let mut far = log.clone(); + far[7].shot = u64::MAX; + assert!(!replay(&o, &far, EngineKind::Interpreter).unwrap().is_empty(), "a shot no run has"); + let mut swapped = log.clone(); + swapped.swap(2, 5); + assert!(!replay(&o, &swapped, EngineKind::Interpreter).unwrap().is_empty(), "shots out of order"); +} + +#[test] +fn the_transcript_s_digest_is_the_text_s_layout() { + // §6.4, written out here from the text, for a run that ends ok and one that faults: the + // receipt's digest is this one, computed from the record alone. + fn layout(calls: &[Call], func: u32, fault: Option<(u64, &str, u64)>) -> String { + let mut h = blake3::Hasher::new(); + h.update(b"wai:wasmq-transcript\x01"); + for c in calls { + h.update(&c.shot.to_le_bytes()); + h.update(&func.to_le_bytes()); + h.update(&(c.input.len() as u32).to_le_bytes()); + h.update(&c.input); + h.update(&(c.output.len() as u32).to_le_bytes()); + h.update(&c.output); + h.update(&c.fuel.to_le_bytes()); + } + if let Some((shot, reason, fuel)) = fault { + h.update(b"fault"); + h.update(&shot.to_le_bytes()); + h.update(reason.as_bytes()); + h.update(&fuel.to_le_bytes()); + } + h.finalize().to_hex().to_string() + } + let key = SigningKey::from_bytes(&[7; 32]); + for (module, per_call) in [(IDENTITY, 500), (SPIN, 500)] { + let mut b = one(program(3, 3, "h q;\nc[0] = measure q[0];\n"), wasm(module), 8); + b.run.per_call = per_call; + let o = open(&pack(&b).unwrap()).unwrap(); + let (job, log) = run(&o, EngineKind::Interpreter, &key, true).unwrap(); + let r = &job["result"]; + let fault = r.get("fault").map(|f| (f["shot"].as_u64().unwrap(), f["reason"].as_str().unwrap(), f["fuel"].as_u64().unwrap())); + assert_eq!(r["transcript_blake3"].as_str().unwrap(), layout(&log.unwrap(), 0, fault), "{module}"); + } +} diff --git a/wai/wai-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs new file mode 100644 index 000000000..84b111cb5 --- /dev/null +++ b/wai/wai-wasmq/tests/vectors.rs @@ -0,0 +1,488 @@ +//! The conformance vectors (`wai/quantum-conformance/wasmq/`) hold on every engine of this +//! build, and are what this file makes: every committed vector is compared with the bytes +//! made here, and every refusal is made beside the same module or bundle without its one +//! defect, which is accepted. `WASMQ_BLESS=1` writes the files; without it the tests only +//! read them. + +use serde_json::{Value, json}; +use std::path::{Path, PathBuf}; +use wai_quantum::quantum_qec::CssCode; +use wai_wasmq::envelope::{Bundle, Callout, MAGIC, Module, Run, from_parts, pack, parts, write_unchecked}; +use wai_wasmq::reference; +use wai_wasmq::run::EngineKind; + +fn dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq") +} + +fn wasm(wat: &str) -> Vec { + wat::parse_str(wat).unwrap() +} + +const PROGRAM: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nh q;\nb = measure q;\nr = f(b);\n"; +const LIST: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[72]) -> bit[72];\nqubit[72] q;\nbit[72] b;\nbit[72] r;\nh q;\nb = measure q;\nr = f(b);\n"; +const TWO: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nextern g(bit[8]) -> bit[8];\nqubit[8] q;\nbit[8] b;\nbit[8] r;\nh q;\nb = measure q;\nr = f(b);\nr = g(r);\n"; +/// A reset whose outcome is random, then resets of qubits still in |0>, then measurements +/// (§6.1): which of them take a value from the shot's stream. +const RESETS: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[8]) -> bit[8];\nqubit[8] q;\nqubit a;\nbit[8] b;\nbit[8] r;\nh a;\nreset a;\nreset q;\nh q;\nb = measure q;\nr = f(b);\n"; +const IDENTITY: &str = r#"(module (func (export "f") (param i64) (result i64) local.get 0))"#; +const BOTH: &str = r#"(module (func (export "f") (param i64) (result i64) local.get 0) (func (export "g") (param i64) (result i64) local.get 0))"#; +/// A list-form identity: the result is the argument's bytes where the host wrote them. +const ECHO: &str = r#"(module (memory (export "memory") 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "f") (param i32 i32) (result i32) + i32.const 2048 local.get 0 i32.store i32.const 2052 local.get 1 i32.store i32.const 2048))"#; + +fn one(program: &str, module: Vec, run: Run) -> Bundle { + Bundle { program: program.into(), inputs: Default::default(), modules: vec![Module { name: "m".into(), bytes: module, priors: vec![] }], callouts: vec![Callout { name: "f".into(), module: 0, export: "f".into() }], run } +} + +fn runs() -> Vec<(&'static str, Bundle)> { + let d = std::fs::read(dir().join("modules/qldpc_decoder.wasm")).unwrap(); + let p = std::fs::read(dir().join("modules/atom_planner.wasm")).unwrap(); + let s3 = CssCode::surface(3); + let s5 = CssCode::surface(5); + let g = CssCode::gross(); + let mut job = Run::new(16, 77); + job.state = "job".into(); + let mut tight = Run::new(32, 9); + tight.per_call = 400; + let spin = r#"(module (func (export "f") (param i64) (result i64) (local i64) + block loop local.get 0 i64.eqz br_if 1 local.get 0 i64.const 1 i64.sub local.set 0 br 0 end end local.get 1))"#; + let deep = r#"(module (func $r (param i64) (result i64) local.get 0 i64.const 1 i64.add call $r) (func (export "f") (param i64) (result i64) local.get 0 call $r))"#; + let nan = r#"(module (func (export "f") (param i64) (result i64) f64.const nan:0x4000000000abc local.get 0 f64.convert_i64_u f64.add i64.reinterpret_f64 i64.const 255 i64.and))"#; + let grow = r#"(module (memory 1 3) (func (export "f") (param i64) (result i64) local.get 0 i32.wrap_i64 i32.const 7 i32.and memory.grow i64.extend_i32_s i64.const 255 i64.and))"#; + let divide = r#"(module (func (export "f") (param i64) (result i64) i64.const 1 local.get 0 i64.const 1 i64.and i64.div_u))"#; + let wide = r#"(module (func (export "f") (param i64) (result i64) local.get 0 i64.const 256 i64.or))"#; + vec![ + ("surface3-decode64.wai", reference::qec_bundle(&d, s3.n, &s3.checks, 1, Run::new(32, 1234))), + ("surface5-decode64-job.wai", reference::qec_bundle(&d, s5.n, &s5.checks, 2, job)), + ("gross-decode.wai", reference::qec_bundle(&d, g.n, &g.checks, 3, Run::new(4, 5))), + ("atoms4x4-plan64.wai", reference::atom_bundle(&p, 4, 4, 2, 2, Run::new(32, 99))), + ("atoms9x9-plan.wai", reference::atom_bundle(&p, 9, 9, 3, 3, Run::new(6, 99))), + ("list-echo.wai", one(LIST, wasm(ECHO), Run::new(8, 3))), + ("reset-draws.wai", one(RESETS, wasm(IDENTITY), Run::new(64, 11))), + ("nan-canonical.wai", one(PROGRAM, wasm(nan), Run::new(8, 2))), + ("grow-past-maximum.wai", one(PROGRAM, wasm(grow), Run::new(16, 5))), + ("fault-fuel.wai", one(PROGRAM, wasm(spin), tight)), + ("fault-stack.wai", one(PROGRAM, wasm(deep), Run::new(2, 1))), + ("fault-trap.wai", one(PROGRAM, wasm(divide), Run::new(64, 4))), + ("fault-bad-result.wai", one(PROGRAM, wasm(wide), Run::new(2, 1))), + ] + .into_iter() + .chain(more_runs()) + .collect() +} + +/// A module of one export `f` of `(i64) -> i64`, its body `body`, with `extra` beside it. +fn scalar(extra: &str, body: &str) -> Vec { + wasm(&format!(r#"(module {extra} (func (export "f") (param i64) (result i64) {body}))"#)) +} + +/// The list-form identity of `LIST` with its result changed: `ret` the address it returns, +/// and `after` what it does to the result's bytes before it returns. +fn echo_with(ret: u32, after: &str) -> Vec { + wasm(&format!( + r#"(module (memory (export "memory") 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "f") (param i32 i32) (result i32) + i32.const 2048 local.get 0 i32.store i32.const 2052 local.get 1 i32.store {after} i32.const {ret}))"# + )) +} + +/// Runs that show what the rest of the text says a run does (§5, §6): every reason a fault +/// has, every phase and every budget it stops in, the calls a host makes into a module, the +/// lowering of a signed argument, the order modules are made in, and a program that +/// measures nothing. +fn more_runs() -> Vec<(&'static str, Bundle)> { + let mut out = Vec::new(); + // Each trap, as a fault of its own reason. + let ty = "(type $t (func (param i64) (result i64)))"; + for (name, extra, body) in [ + ("fault-unreachable.wai", "", "unreachable"), + ("fault-memory-out-of-bounds.wai", "(memory 1 1)", "i32.const 70000 i64.load"), + ("fault-table-out-of-bounds.wai", &*format!("{ty} (table 1 1 funcref)"), "local.get 0 i32.const 5 call_indirect (type $t)"), + ("fault-indirect-call-null.wai", &*format!("{ty} (table 1 1 funcref)"), "local.get 0 i32.const 0 call_indirect (type $t)"), + ("fault-indirect-call-type.wai", &*format!("{ty} (table 1 1 funcref) (func $g) (elem (i32.const 0) func $g)"), "local.get 0 i32.const 0 call_indirect (type $t)"), + ("fault-integer-overflow.wai", "", "i64.const 0x8000000000000000 i64.const -1 i64.div_s"), + ("fault-invalid-conversion.wai", "", "f64.const nan i64.trunc_f64_s"), + ] { + out.push((name, one(PROGRAM, scalar(extra, body), Run::new(4, 1)))); + } + // A fault in each phase of making an instance (§5.3). + out.push(("fault-in-seed.wai", one(PROGRAM, scalar(r#"(func (export "wasmq-seed") (param i64) unreachable)"#, "local.get 0"), Run::new(4, 1)))); + out.push(("fault-in-init.wai", one(PROGRAM, scalar(r#"(func (export "init") unreachable)"#, "local.get 0"), Run::new(4, 1)))); + let mut prior = one( + PROGRAM, + scalar(r#"(memory (export "memory") 1 1) (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) (func (export "wasmq-prior") (param i32 i32 i32) unreachable)"#, "local.get 0"), + Run::new(4, 1), + ); + prior.modules[0].priors.push(("weights".into(), vec![1, 2, 3, 4, 5])); + out.push(("fault-in-prior.wai", prior)); + // Each budget a fuel fault can be of (§4.2): an instance's, and what is left of the run's. + let mut instance = Run::new(4, 1); + instance.per_init = 300; + out.push(("fault-fuel-instance.wai", one(PROGRAM, scalar(r#"(func (export "init") loop br 0 end)"#, "local.get 0"), instance))); + let mut run = Run::new(8, 1); + run.per_job = 50; + let sum = "local.get 0 ".to_string() + &"local.get 0 i64.add ".repeat(10) + "drop local.get 0"; + out.push(("fault-fuel-run.wai", one(PROGRAM, scalar("", &sum), run))); + // The calls a host makes into a module around a list (§5.2): `cabi_post_f` after the + // result is read, which uses fuel of the call's. + let post = wasm( + r#"(module (memory (export "memory") 1 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 1024) + (func (export "f") (param i32 i32) (result i32) i32.const 2048 local.get 0 i32.store i32.const 2052 local.get 1 i32.store i32.const 2048) + (func (export "cabi_post_f") (param i32) (local i32) loop local.get 1 i32.const 1 i32.add local.tee 1 i32.const 5 i32.lt_u br_if 0 end))"#, + ); + out.push(("list-post.wai", one(LIST, post, Run::new(8, 3)))); + // Each way a list result is not one (§5.2). + for (name, m) in [ + // The pointer and length written at 2050, so that only the address's alignment is wrong. + ("fault-list-misaligned.wai", echo_with(2050, "i32.const 2050 local.get 0 i32.store i32.const 2054 local.get 1 i32.store")), + ("fault-list-outside.wai", echo_with(70000, "")), + ("fault-list-length.wai", echo_with(2048, "i32.const 2052 i32.const 5 i32.store")), + ] { + out.push((name, one(LIST, m, Run::new(4, 3)))); + } + // Seventy bits take nine bytes, the last two bits of the last byte zero. + let seventy = LIST.replace("72", "70"); + out.push(("fault-list-past-its-bits.wai", one(&seventy, echo_with(2048, "i32.const 1032 i32.const 255 i32.store8"), Run::new(4, 3)))); + // A signed argument, sign-extended (§5.2): `f(-3, 200)` gives back its two arguments. + let signed = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(int[8], uint[8]) -> bit[64];\nqubit[1] q;\nbit[64] r;\nr = f(-3, 200);\n"; + out.push(("signed-argument.wai", one(signed, wasm(r#"(module (func (export "f") (param i64 i64) (result i64) local.get 0 i64.const 8 i64.shl local.get 1 i64.or))"#), Run::new(2, 1)))); + // A program that measures nothing: its outcome is its bits, which its call writes, and + // no qubit is read for it (§6.1). + let unmeasured = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[3] q;\nbit[1] d;\nd = f(d);\nh q;\n"; + out.push(("measures-nothing.wai", one(unmeasured, wasm(IDENTITY), Run::new(16, 7)))); + // Bits nothing writes are in the outcome all the same, as 0 (§6.1): `a`'s three, then + // the call's two, so every outcome is `11000`. + let unwritten = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(uint[8]) -> bit[2];\nqubit[1] q;\nbit[3] a;\nbit[2] r;\nr = f(3);\n"; + out.push(("bits-nothing-writes.wai", one(unwritten, wasm(IDENTITY), Run::new(2, 7)))); + // A block's own bit is no bit of the outcome (§6.1, Q322): the loop's `t` is measured and + // read, and the outcome is `a` and `b` alone, `11`. + let block = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nqubit[1] q;\nbit[1] a;\nx q[0];\nfor uint i in [0:0] {\n bit t;\n t = measure q[0];\n if (t) { a = measure q[0]; }\n}\nbit[1] b;\nb = f(a);\n"; + out.push(("block-bit-not-in-outcome.wai", one(block, wasm(IDENTITY), Run::new(2, 7)))); + // Two modules, each failing in its init: they are made in the contract's order (§5.3). + let fails = |m: &str| wasm(&format!(r#"(module (func (export "init") unreachable) (func (export "{m}") (param i64) (result i64) local.get 0))"#)); + let mut two = one(TWO, fails("f"), Run::new(4, 1)); + two.modules[0].name = "first".into(); + two.modules.push(Module { name: "second".into(), bytes: fails("g"), priors: vec![] }); + two.callouts = vec![Callout { name: "f".into(), module: 0, export: "f".into() }, Callout { name: "g".into(), module: 1, export: "g".into() }]; + out.push(("two-modules-in-order.wai", two)); + // A program that declares no qubit and names physical ones: its qubits are one more than + // the highest index it names (§2.2), here 3; and its bits are numbered in the order it + // declares them (§6.1), so the measured `b[1]` is bit 2 and every outcome is `100`. + let physical = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(bit[1]) -> bit[1];\nbit[1] a;\nbit[2] b;\nx $2;\nb[1] = measure $2;\na = f(a);\n"; + out.push(("physical-qubits.wai", one(physical, wasm(IDENTITY), Run::new(4, 7)))); + out +} + +fn leb(mut v: u64) -> Vec { + let mut out = Vec::new(); + loop { + let b = (v & 0x7f) as u8; + v >>= 7; + if v == 0 { + out.push(b); + return out; + } + out.push(b | 0x80); + } +} + +fn raw(sections: &[(u8, Vec)]) -> Vec { + let mut m = b"\0asm\x01\0\0\0".to_vec(); + for (id, body) in sections { + m.push(*id); + m.extend(leb(body.len() as u64)); + m.extend_from_slice(body); + } + m +} + +/// A module a runner refuses: its name, its code, the module, the same module without +/// the defect, and its text when it has one. +struct Refused { + name: &'static str, + code: &'static str, + module: Vec, + repaired: Vec, + wat: Option, +} + +fn module_refusals() -> Vec { + let f = r#"(func (export "f") (param i64) (result i64) local.get 0)"#; + let text = |name: &'static str, code: &'static str, defect: String, repaired: String| Refused { name, code, module: wasm(&defect), repaired: wasm(&repaired), wat: Some(defect) }; + let with = |name: &'static str, code: &'static str, defect: &str, repaired: &str| text(name, code, format!("(module {defect} {f})"), format!("(module {repaired} {f})")); + let body = |name: &'static str, code: &'static str, defect: &str, repaired: &str| { + text(name, code, format!(r#"(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) {defect}))"#), format!(r#"(module (memory 1 1) (table 1 funcref) (func (export "f") (param i64) (result i64) {repaired}))"#)) + }; + let exports = |n: u32| format!("(module (func) {})", (0..n).map(|k| format!("(export \"e{k}\" (func 0)) ")).collect::()); + let locals = |n: usize| format!("(module (func (local {})))", "i64 ".repeat(n)); + // One type `() -> ()`, one function of it, and a body: `i32.const 0`, `drop`, and an `end` or none. + let unfinished = |end: bool| { + let mut code = vec![0x01, if end { 0x05 } else { 0x04 }, 0x00, 0x41, 0x00, 0x1a]; + if end { + code.push(0x0b); + } + raw(&[(1, vec![0x01, 0x60, 0x00, 0x00]), (3, vec![0x01, 0x00]), (10, code)]) + }; + // One type, and one function of type `index`. + let typed = |index: u8| raw(&[(1, vec![0x01, 0x60, 0x00, 0x00]), (3, vec![0x01, index]), (10, vec![0x01, 0x02, 0x00, 0x0b])]); + vec![ + Refused { name: "body-without-end", code: "W001", module: unfinished(false), repaired: unfinished(true), wat: None }, + Refused { name: "function-of-no-type", code: "W001", module: typed(5), repaired: typed(0), wat: None }, + with("import-clock", "W002", r#"(import "wasi:clocks/monotonic-clock" "now" (func (result i64)))"#, ""), + with("import-random", "W002", r#"(import "wasi_snapshot_preview1" "random_get" (func (param i32 i32) (result i32)))"#, ""), + with("memory-unbounded", "W003", "(memory 1)", "(memory 1 1)"), + with("memory-too-large", "W003", "(memory 1 4097)", "(memory 1 4096)"), + with("start-function", "W004", "(func $s) (start $s)", "(func $s)"), + with("shared-memory", "W005", "(memory 1 1 shared)", "(memory 1 1)"), + body("atomic", "W005", "i32.const 0 local.get 0 i64.atomic.rmw.add", "i32.const 0 i64.load local.get 0 i64.add"), + body("simd", "W006", "local.get 0 i64x2.splat i64x2.extract_lane 0", "local.get 0"), + body("relaxed-simd", "W006", "local.get 0 i64x2.splat local.get 0 i64x2.splat local.get 0 i64x2.splat f32x4.relaxed_madd i64x2.extract_lane 0", "local.get 0"), + with("tail-call", "W007", "(func $g (param i64) (result i64) local.get 0) (func (param i64) (result i64) local.get 0 return_call $g)", "(func $g (param i64) (result i64) local.get 0) (func (param i64) (result i64) local.get 0 call $g)"), + body("ref-null", "W007", "ref.null func ref.is_null i64.extend_i32_u", "i32.const 1 i64.extend_i32_u"), + with("reference-type", "W007", "(func (param externref))", "(func (param i64))"), + body("table-grow", "W008", "unreachable table.grow 0 i64.extend_i32_u", "unreachable i64.extend_i32_u"), + body("table-size", "W008", "table.size 0 i64.extend_i32_u", "memory.size i64.extend_i32_u"), + text("frame-too-large", "W009", locals(4097), locals(4096)), + text("too-many-exports", "W010", exports(1025), exports(1024)), + with("data-past-memory", "W011", r#"(memory 1 1) (data (i32.const 65530) "0123456789")"#, r#"(memory 1 1) (data (i32.const 65526) "0123456789")"#), + with("reserved-export", "W012", r#"(global (export "__wasmq_fuel") (mut i64) (i64.const 0))"#, r#"(global (export "fuel") (mut i64) (i64.const 0))"#), + with("seed-of-another-type", "W012", r#"(func (export "wasmq-seed") (param i32))"#, r#"(func (export "wasmq-seed") (param i64))"#), + ] +} + +/// A bundle that is well formed and breaks a rule: its name, its code, the bundle, and +/// the same bundle without the defect. +fn bundle_refusals() -> Vec<(&'static str, &'static str, Bundle, Bundle)> { + let good = || one(PROGRAM, wasm(IDENTITY), Run::new(4, 1)); + let two = || { + let mut b = one(TWO, wasm(BOTH), Run::new(4, 1)); + b.callouts.push(Callout { name: "g".into(), module: 0, export: "g".into() }); + b + }; + let mut out = Vec::new(); + let mut add = |name: &'static str, code: &'static str, defect: Bundle, repaired: Bundle| out.push((name, code, defect, repaired)); + let mut swapped = two(); + swapped.callouts.reverse(); + add("callouts-out-of-order", "W015", swapped, two()); + let mut twice = good(); + twice.callouts.push(Callout { name: "f".into(), module: 0, export: "f".into() }); + add("two-callouts-for-one-extern", "W015", twice, good()); + add("callout-of-another-type", "W015", one(PROGRAM, wasm(r#"(module (func (export "f") (param i32) (result i32) local.get 0))"#), Run::new(4, 1)), good()); + add("memory-not-exported", "W015", one(LIST, wasm(&ECHO.replace("(memory (export \"memory\") 1 1)", "(memory 1 1)")), Run::new(4, 1)), one(LIST, wasm(ECHO), Run::new(4, 1))); + add("gate-outside-the-clifford-gates", "W016", one(&PROGRAM.replace("h q;", "h q;\nt q[0];"), wasm(IDENTITY), Run::new(4, 1)), good()); + add("program-calls-nothing", "W016", one(&PROGRAM.replace("r = f(b);", "r = measure q;"), wasm(IDENTITY), Run::new(4, 1)), good()); + // A measurement that names no bit, in a program that calls externs (§6.1, Q324): its + // outcome is the bits it declares, and this one declares none for it. + let bare = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(uint[8]);\nqubit[4] q;\nf(1);\nx q[3];\nmeasure q[3];\n"; + let named = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nextern f(uint[8]);\nqubit[4] q;\nbit[4] c;\nf(1);\nx q[3];\nc[3] = measure q[3];\n"; + let sink = || wasm(r#"(module (func (export "f") (param i64)))"#); + add("program-measure-with-no-bit", "W016", one(bare, sink(), Run::new(2, 7)), one(named, sink(), Run::new(2, 7))); + add("no-shots", "W017", one(PROGRAM, wasm(IDENTITY), Run::new(0, 1)), good()); + let mut state = good(); + state.run.state = "forever".into(); + add("instances-of-no-kind", "W017", state, good()); + let mut seed = good(); + seed.run.seed = 1 << 53; + add("seed-a-double-cannot-hold", "W017", seed, good()); + // What a host must hold and do that fuel does not count (§4.7, §4.8). + let of_memory = |pages: u32| { + let m = format!(r#"(module (memory (export "memory") {pages} {pages}) (func (export "f") (param i64) (result i64) local.get 0) (func (export "g") (param i64) (result i64) local.get 0))"#); + let mut b = one(TWO, wasm(&m), Run::new(4, 1)); + b.modules.push(Module { name: "n".into(), bytes: wasm(&m), priors: vec![] }); + b.callouts = vec![Callout { name: "f".into(), module: 0, export: "f".into() }, Callout { name: "g".into(), module: 1, export: "g".into() }]; + b + }; + add("memory-past-a-run-s-among-its-modules", "W003", of_memory(2049), of_memory(2048)); + // The most bits a program holds, a shot more than results may hold. + let wide = PROGRAM.replace("bit[8] r;", &format!("bit[8] r;\nbit[{}] pad;", (1 << 20) - 16)); + add("results-past-what-a-host-holds", "W017", one(&wide, wasm(IDENTITY), Run::new((1 << 8) + 1, 1)), one(&wide, wasm(IDENTITY), Run::new(1 << 8, 1))); + let measures = PROGRAM.replace("qubit[8] q;", "qubit[32768] q;").replace("h q;\nb = measure q;", "b[0] = measure q[0];"); + // Eight gates, eight measurements and a call, and resets up to the steps a program takes. + let resets = |n: u64| PROGRAM.replace("r = f(b);", &format!("r = f(b);\nfor uint i in [0:{}] {{ reset q[0]; }}", n - 1)); + add("program-past-its-steps", "W016", one(&resets((1 << 21) - 16), wasm(IDENTITY), Run::new(1, 1)), one(&resets((1 << 21) - 17), wasm(IDENTITY), Run::new(1, 1))); + add("work-past-what-a-run-may-ask", "W017", one(&measures, wasm(IDENTITY), Run::new(1 << 20, 1)), one(&measures, wasm(IDENTITY), Run::new(4, 1))); + out +} + +/// Envelopes one edit away from one that opens: each name, its code, and its bytes. +fn edited(base: &[u8]) -> Vec<(&'static str, &'static str, Vec)> { + let (manifest, contract, frames) = parts(base).unwrap(); + let text = String::from_utf8(contract.clone()).unwrap(); + let with = |manifest: &[u8], magic: &[u8; 4], contract: &[u8], frames: &[u8]| from_parts(manifest, magic, contract, frames).unwrap(); + let flip = |find: &[u8], with: &[u8]| { + let at = base.windows(find.len()).position(|w| w == find).unwrap(); + let mut e = base.to_vec(); + e[at..at + with.len()].copy_from_slice(with); + e + }; + let prior_at = base.windows(15).position(|w| w == b"wai:qec-checks\x01").unwrap(); + let mut wrong_prior = base.to_vec(); + wrong_prior[prior_at + 15 + 8 + 4] ^= 1; + let module_at = base.windows(8).position(|w| w == b"\0asm\x01\0\0\0").unwrap(); + let mut wrong_module = base.to_vec(); + wrong_module[module_at + 200] ^= 0x40; + let mut trailing = base.to_vec(); + trailing.push(0); + let mut more = frames.clone(); + more.push(0); + let shots = text.replacen("\"shots\":32", "\"shots\":33", 1); + let extra = text.replacen(",\"modules\":", ",\"extra\":0,\"modules\":", 1); + assert!(shots != text && extra != text); + let m = String::from_utf8(manifest.clone()).unwrap(); + vec![ + ("prior-hash-mismatch", "W014", wrong_prior), + ("module-hash-mismatch", "W014", wrong_module), + ("program-hash-mismatch", "W014", flip(b"h c0;", b"x c0;")), + ("contract-run-changed", "W013", with(&manifest, MAGIC, shots.as_bytes(), &frames)), + ("contract-extra-member", "W013", with(&manifest, MAGIC, extra.as_bytes(), &frames)), + ("contract-not-canonical", "W013", with(&manifest, MAGIC, text.replacen('{', "{ ", 1).as_bytes(), &frames)), + ("payload-of-another-kind", "W013", with(&manifest, b"WQM2", &contract, &frames)), + ("byte-after-the-frames", "W013", with(&manifest, MAGIC, &contract, &more)), + ("manifest-changed", "W013", with(m.replace("\"shots\":32", "\"shots\":33").as_bytes(), MAGIC, &contract, &frames)), + ("other-capability", "W013", with(m.replace("wai.quantum.wasmq", "wai.quantum.wasmx").as_bytes(), MAGIC, &contract, &frames)), + ("trailing-byte", "W013", trailing), + ("truncated", "W013", base[..base.len() - 1].to_vec()), + ] +} + +/// Every vector file this test makes, by its path under the vectors' directory, and the +/// index that lists them. +fn made() -> (Vec<(String, Vec)>, Value) { + let key = wai_wasmq::vectors::key(); + let mut files = Vec::new(); + let mut index_runs = Vec::new(); + let mut base = Vec::new(); + for (name, b) in runs() { + let env = pack(&b).unwrap_or_else(|e| panic!("{name}: {e}")); + let o = wai_wasmq::open(&env).unwrap(); + let (body, _) = wai_wasmq::run(&o, EngineKind::Interpreter, &key, false).unwrap(); + index_runs.push(json!({ "file": format!("envelopes/{name}"), "envelope_blake3": wai_wasmq::envelope::blake3_hex(&env), "expect": wai_wasmq::vectors::pins(&body) })); + if name == "surface3-decode64.wai" { + files.push(("envelopes/surface3-decode64.job.json".to_string(), (serde_json::to_string_pretty(&body).unwrap() + "\n").into_bytes())); + files.push(("envelopes/surface3-decode64.qasm".to_string(), b.program.clone().into_bytes())); + files.push(("envelopes/surface3-decode64.checks.bin".to_string(), b.modules[0].priors[0].1.clone())); + base = env.clone(); + } + files.push((format!("envelopes/{name}"), env)); + } + let mut index_refuse = Vec::new(); + for r in module_refusals() { + if let Some(wat) = &r.wat { + files.push((format!("refuse/{}.wat", r.name), format!("{wat}\n").into_bytes())); + } + files.push((format!("refuse/{}.wasm", r.name), r.module)); + index_refuse.push(json!({ "file": format!("refuse/{}.wasm", r.name), "code": r.code })); + } + for (name, code, defect, _) in bundle_refusals() { + let qubits = wai_quantum::qasm::read_callouts(&defect.program, &defect.inputs).map(|h| h.reading.program.n_qubits).unwrap_or(8); + files.push((format!("refuse/{name}.wai"), write_unchecked(&defect, wai_wasmq::envelope::METHOD, qubits).unwrap())); + index_refuse.push(json!({ "file": format!("refuse/{name}.wai"), "code": code })); + } + for (name, code, bytes) in edited(&base) { + files.push((format!("refuse/{name}.wai"), bytes)); + index_refuse.push(json!({ "file": format!("refuse/{name}.wai"), "code": code })); + } + let index = json!({ + "profile": wai_wasmq::profile::PROFILE, + "note": "every run vector gives exactly `expect` on every engine and every host; every refusal is refused with its code (extensions/quantum-wasmq.md §9)", + "runs": index_runs, + "refuse": index_refuse, + }); + (files, index) +} + +fn kinds() -> Vec { + wai_wasmq::engine::available().iter().map(|s| EngineKind::parse(s).unwrap()).collect() +} + +#[test] +fn the_vectors_are_the_ones_made_here_and_hold_on_every_engine_of_this_build() { + let d = dir(); + let (files, index) = made(); + let index_text = serde_json::to_string_pretty(&index).unwrap() + "\n"; + if std::env::var("WASMQ_BLESS").is_ok() { + for sub in ["envelopes", "refuse"] { + let _ = std::fs::remove_dir_all(d.join(sub)); + std::fs::create_dir_all(d.join(sub)).unwrap(); + } + for (path, bytes) in &files { + std::fs::write(d.join(path), bytes).unwrap(); + } + std::fs::write(d.join("vectors.json"), &index_text).unwrap(); + } + // The committed files are these, and there are no others. + for (path, bytes) in &files { + assert!(std::fs::read(d.join(path)).is_ok_and(|have| &have == bytes), "{path} is not the file this test makes: run it with WASMQ_BLESS=1"); + } + assert_eq!(std::fs::read_to_string(d.join("vectors.json")).unwrap(), index_text, "vectors.json is not the index this test makes"); + for sub in ["envelopes", "refuse"] { + for entry in std::fs::read_dir(d.join(sub)).unwrap() { + let name = format!("{sub}/{}", entry.unwrap().file_name().to_string_lossy()); + assert!(files.iter().any(|(p, _)| *p == name), "{name} is a file this test does not make"); + } + } + let (lines, problems) = wai_wasmq::vectors::verify(&d, &kinds()).unwrap(); + assert!(problems.is_empty(), "{problems:#?}"); + assert_eq!(lines.len(), index["runs"].as_array().unwrap().len() + index["refuse"].as_array().unwrap().len()); + // Every code of §3.3 has a vector. + let codes: std::collections::BTreeSet<&str> = index["refuse"].as_array().unwrap().iter().filter_map(|r| r["code"].as_str()).collect(); + let all: std::collections::BTreeSet<&str> = wai_wasmq::profile::CODES.iter().map(|c| c.0).collect(); + assert_eq!(codes, all, "the codes with a refusal vector"); + // And every reason a fault has, every phase and every budget it stops in, is shown. + let shown = |member: &str| -> std::collections::BTreeSet { index["runs"].as_array().unwrap().iter().filter_map(|r| r["expect"]["fault"][member].as_str().map(str::to_string)).collect() }; + let want = |v: &[&str]| -> std::collections::BTreeSet { v.iter().map(|s| s.to_string()).collect() }; + assert_eq!(shown("reason"), want(&wai_wasmq::run::REASONS), "the reasons a run vector ends in"); + assert_eq!(shown("phase"), want(&["seed", "prior", "init", "call"]), "the phases a run vector stops in"); + assert_eq!(shown("budget"), want(&["call", "instance", "run"]), "the budgets a fuel fault of a run vector is of"); +} + +#[test] +fn each_refusal_is_refused_for_its_one_defect() { + // A module vector: the module has its code, and the same module without the defect passes. + for r in module_refusals() { + assert_eq!(wai_wasmq::profile::check(&r.module).err().map(|e| e.code), Some(r.code), "{}", r.name); + if let Err(e) = wai_wasmq::profile::check(&r.repaired) { + panic!("{}: without its defect the module is still refused: {e}", r.name); + } + } + // A bundle vector: the bundle has its code, and the same bundle without the defect packs. + for (name, code, defect, repaired) in bundle_refusals() { + let qubits = wai_quantum::qasm::read_callouts(&defect.program, &defect.inputs).map(|h| h.reading.program.n_qubits).unwrap_or(8); + let env = write_unchecked(&defect, wai_wasmq::envelope::METHOD, qubits).unwrap(); + assert_eq!(wai_wasmq::open(&env).err().map(|e| e.code), Some(code), "{name}"); + if let Err(e) = pack(&repaired) { + panic!("{name}: without its defect the bundle is still refused: {e}"); + } + } + // An edited envelope: one edit away from an envelope that opens. + let base = pack(&runs().remove(0).1).unwrap(); + assert!(wai_wasmq::open(&base).is_ok()); + for (name, code, bytes) in edited(&base) { + assert_eq!(wai_wasmq::open(&bytes).err().map(|e| e.code), Some(code), "{name}"); + } +} + +/// What a receipt shows of its envelope and its transcript is what the writer wrote: each run +/// vector's envelope is as long as its job says (rule 31), and where no call returned, its +/// transcript is the one rule 35 reads. +#[test] +fn a_job_shows_its_envelope_s_length_and_a_transcript_of_no_call() { + let key = wai_wasmq::vectors::key(); + let mut bare = 0; + for (name, b) in runs() { + let env = pack(&b).unwrap_or_else(|e| panic!("{name}: {e}")); + let (body, _) = wai_wasmq::run(&wai_wasmq::open(&env).unwrap(), EngineKind::Interpreter, &key, false).unwrap(); + let (c, r) = (&body["request"]["wasmq"], &body["result"]); + assert_eq!(wai_quantum::engine::wasmq::envelope_bytes(Some(c), r).0, env.len() as u128, "{name}"); + if let Some(t) = wai_quantum::engine::wasmq::bare_transcript(r) { + assert_eq!(r["transcript_blake3"].as_str(), Some(t.as_str()), "{name}"); + bare += 1; + } + } + assert!(bare >= 4, "the run vectors with no call that returned: {bare}"); +} diff --git a/wai/wai-wasmq/tests/wit.rs b/wai/wai-wasmq/tests/wit.rs new file mode 100644 index 000000000..944eafaca --- /dev/null +++ b/wai/wai-wasmq/tests/wit.rs @@ -0,0 +1,87 @@ +//! The WIT file (extensions/quantum-wasmq.wit) is the interface the modules have: each +//! world's functions lower, by the Component Model's canonical ABI, to exactly the core +//! types the reference modules export and an `extern` of the same shape lowers to (§5.2), +//! and its fault reasons are the ones a receipt names. + +use wai_quantum::qasm::{ExternArg, ExternDecl}; +use wai_wasmq::{abi, profile}; +use wit_parser::abi::{AbiVariant, WasmType}; +use wit_parser::{Resolve, WorldItem, WorldKey}; + +const WIT: &str = include_str!("../../extensions/quantum-wasmq.wit"); +const DECODER: &[u8] = include_bytes!("../../quantum-conformance/wasmq/modules/qldpc_decoder.wasm"); +const PLANNER: &[u8] = include_bytes!("../../quantum-conformance/wasmq/modules/atom_planner.wasm"); + +fn val(t: WasmType) -> wasmparser::ValType { + match t { + WasmType::I32 | WasmType::Pointer | WasmType::Length => wasmparser::ValType::I32, + WasmType::I64 | WasmType::PointerOrI64 => wasmparser::ValType::I64, + WasmType::F32 => wasmparser::ValType::F32, + WasmType::F64 => wasmparser::ValType::F64, + } +} + +/// A core function type: its parameters and results. +type Core = (Vec, Vec); + +fn exports(resolve: &Resolve, world: &str) -> Vec<(String, Core)> { + let pkg = resolve.packages.iter().next().unwrap().0; + let w = resolve.select_world(&[pkg], Some(world)).unwrap(); + let mut out = Vec::new(); + for (k, item) in &resolve.worlds[w].exports { + if let (WorldKey::Name(name), WorldItem::Function(f)) = (k, item) { + let s = resolve.wasm_signature(AbiVariant::GuestExport, f); + out.push((name.clone(), (s.params.into_iter().map(val).collect(), s.results.into_iter().map(val).collect()))); + } + } + out +} + +fn resolve() -> Resolve { + let mut r = Resolve::default(); + r.push_str("quantum-wasmq.wit", WIT).unwrap(); + r +} + +#[test] +fn the_reference_modules_export_what_their_worlds_lower_to() { + let r = resolve(); + for (world, module) in [("qldpc-decoder", DECODER), ("atom-planner", PLANNER)] { + let facts = profile::check(module).unwrap(); + let ex = exports(&r, world); + assert!(ex.len() >= 3, "{world}"); + for (name, sig) in ex { + if name == "init" { + continue; // optional, and neither reference module needs it + } + assert_eq!(facts.functions.get(&name), Some(&sig), "{world}: {name}"); + } + } +} + +#[test] +fn an_extern_lowers_as_its_wit_function_does() { + let r = resolve(); + let ex: std::collections::BTreeMap<_, _> = exports(&r, "qldpc-decoder").into_iter().collect(); + let scalar = ExternDecl { name: "decode64".into(), args: vec![ExternArg::Bit(8)], ret: Some(13) }; + assert_eq!(abi::core_type(&scalar), ex["decode64"]); + let list = ExternDecl { name: "decode".into(), args: vec![ExternArg::Bit(72)], ret: Some(144) }; + assert_eq!(abi::core_type(&list), ex["decode"]); + // The lifecycle the runner calls is the one the module world declares. + let m: std::collections::BTreeMap<_, _> = exports(&r, "module").into_iter().collect(); + assert_eq!(m["wasmq-prior"].0.len(), 3); + assert_eq!(m["wasmq-seed"], (vec![wasmparser::ValType::I64], vec![])); +} + +#[test] +fn the_fault_reasons_are_the_ones_a_receipt_names() { + let r = resolve(); + let iface = r.interfaces.iter().find(|(_, i)| i.name.as_deref() == Some("receipt")).unwrap().1; + let ty = iface.types["fault-reason"]; + let wit_parser::TypeDefKind::Enum(e) = &r.types[ty].kind else { panic!("fault-reason is an enum") }; + let wit: Vec = e.cases.iter().map(|c| c.name.replace('-', "_")).collect(); + use wai_wasmq::engine::Trap::*; + let mut ours: Vec = [Unreachable, MemoryOutOfBounds, TableOutOfBounds, IndirectCallNull, IndirectCallType, DivideByZero, IntegerOverflow, InvalidConversion, Fuel, Stack].iter().map(|t| t.reason().to_string()).collect(); + ours.push("bad_result".into()); + assert_eq!(wit, ours); +} diff --git a/wai/wai-web/compose/README.md b/wai/wai-web/compose/README.md index 97312aed0..a45fd8167 100644 --- a/wai/wai-web/compose/README.md +++ b/wai/wai-web/compose/README.md @@ -20,7 +20,7 @@ cut at the lead's piece starts, each piece one object: | component | capability | source | presented as | |---|---|---|---| -| `v` | `wai.video.int_motion` bases and a `wai.video.av1` floor for every piece | the fixture `compose-01`, a 320 × 180 test pattern at 25 frames a second | the picture, and the clock | +| `v` | `wai.video.int_motion` bases and a `wai.video.av1` floor for every piece | the fixture `compose-01`, a test pattern of 320 × 180 at 25 frames a second | the picture, and the clock | | `g` | `wai.graphics.vector` | `vector-conformance/vectors/motion/stroke-titles.wvg1`, a title card and a credit roll at 1280 × 720 | an overlay rasterised in WebAssembly | | `t` | `wai.text.timed` | `sources/t.vtt`, three captions | text, in the page's text layer | | `s` | `wai.audio.score`, and a `wai.audio.flac` floor of each piece's mixdown | `sources/s.wscr`, `score-conformance/vectors/scale.wscr`'s notes up and down | samples synthesised in the tab, on Web Audio | diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index d58de530d..60677b59b 100644 --- a/wai/wai-web/src/quantum_engine.rs +++ b/wai/wai-web/src/quantum_engine.rs @@ -341,7 +341,9 @@ pub fn engine_repair(src: &str, options: &str) -> String { /// Whether a job's receipt seals its request and its result and every rule /// holds, checked here on its text: `{"valid", "reasons", "key", "form", -/// "signer"}` (the signer as a `did:key`), or `{"error"}`. +/// "signer"}` (the signer as a `did:key`). Text that does not read as a +/// job's, a body that is no job, a result with no receipt: each is a verdict +/// with that reason alone, as on every surface. #[wasm_bindgen] pub fn engine_verify(job_json: &str) -> String { job::verdict_of_text(job_json).map(|v| v.to_string()).unwrap_or_else(error_json) @@ -793,6 +795,15 @@ mod tests { assert_eq!(said("crafted-read-duplicate.json")["reasons"], json!(["unreadable"])); assert_eq!(said("crafted-number-inexact.json")["reasons"], json!(["inexact_number"])); assert_eq!(said("crafted-provider-measured-simulator.json")["reasons"], json!(["provider_class"])); + // A wasmQ job: held to its profile's form (quantum-wasmq §6.5), its run stated beside. + assert_eq!(said("cli-wasmq-run.json")["valid"], true); + assert_eq!(said("crafted-wasmq-counts-short.json")["reasons"], json!(["wasmq_form"])); + assert_eq!(said("crafted-wasmq-envelope-changed.json")["wasmq"]["outcome"], "fault"); + // Text that does not read is a verdict, as on every surface: unreadable, not an error. + for text in ["not json", "\u{feff}{}"] { + let v: Value = serde_json::from_str(&engine_verify(text)).unwrap(); + assert_eq!((v["valid"].as_bool(), v["reasons"].clone()), (Some(false), json!(["unreadable"])), "{v}"); + } } /// A check every build answers with the same bytes: the noisy lesson's