From 0c9e94e861a682f82b80c3f890c205f03d2ae87c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 14:47:47 -0500 Subject: [PATCH 01/17] wai: wasmQ, an OpenQASM 3 program and the WebAssembly its externs call, run the same everywhere, metered and receipted A new capability, wai.quantum.wasmq (extensions/quantum-wasmq.md, its WIT beside it): one WAI1 envelope carries an OpenQASM 3 program, the WebAssembly modules that define its `extern` functions and their priors, each pinned by BLAKE3 in a canonical contract. - The profile: a core Lime1 module that imports nothing, bounds its memory and has no start function, threads, vectors, references or growing table, each rule a refusal code. - The prepared module meters its own fuel at the start of each run of instructions, counts its own call depth and returns only canonical NaNs, so any engine runs it alike. The fuel equals the pinned wasmtime's own default count. - Two engines, the pinned wasmtime and an interpreter of the crate's own that also runs on wasm32, agree on every instruction, charge, trap and fault. - The reader takes extern calls as steps when asked (qasm::read_callouts); its existing types are unchanged. - A run seals the engine's job receipt with the fuel, the modules' digests, a digest of every call and the fault; a callout over budget ends in a signed fault receipt. - wai-quantum pack-wasmq, check-wasmq, run ENVELOPE.wai (--engine, --transcript, --replay) and verify --bundle. - Reference modules from the engine's own decoder and atom planner, modules of record built on x86_64 Linux; vectors in quantum-conformance/wasmq/; a workflow that runs them on linux, macOS and wasm32, and rebuilds the modules. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-wasmq.yml | 202 +++ scripts/check-wasmtime-pin-selftest.sh | 7 + scripts/check-wasmtime-pin.py | 31 +- wai/REGISTRY.md | 1 + wai/SPEC.md | 1 + ...26-11-05-wasmq-hybrid-quantum-classical.md | 40 + wai/extensions/quantum-providers.md | 4 + wai/extensions/quantum-wasmq.md | 506 +++++++ wai/extensions/quantum-wasmq.wit | 60 + wai/quantum-conformance/README.md | 4 +- wai/quantum-conformance/wasmq/.gitattributes | 2 + wai/quantum-conformance/wasmq/README.md | 27 + .../wasmq/envelopes/atoms4x4-plan64.wai | Bin 0 -> 25364 bytes .../wasmq/envelopes/atoms9x9-plan.wai | Bin 0 -> 25376 bytes .../wasmq/envelopes/fault-depth.wai | Bin 0 -> 867 bytes .../wasmq/envelopes/fault-fuel.wai | Bin 0 -> 860 bytes .../wasmq/envelopes/gross-decode.wai | Bin 0 -> 55460 bytes .../wasmq/envelopes/grow-past-maximum.wai | Bin 0 -> 860 bytes .../wasmq/envelopes/nan-canonical.wai | Bin 0 -> 857 bytes .../envelopes/surface3-decode64.job.json | 117 ++ .../wasmq/envelopes/surface3-decode64.qasm | 61 + .../wasmq/envelopes/surface3-decode64.wai | Bin 0 -> 41454 bytes .../wasmq/envelopes/surface5-decode64-job.wai | Bin 0 -> 43836 bytes .../wasmq/modules/atom_planner.wasm | Bin 0 -> 24165 bytes .../wasmq/modules/modules.json | 30 + .../wasmq/modules/qldpc_decoder.wasm | Bin 0 -> 39442 bytes .../wasmq/refuse/atomic.wasm | Bin 0 -> 47 bytes .../wasmq/refuse/atomic.wat | 1 + .../wasmq/refuse/contract-run-changed.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/data-past-memory.wasm | Bin 0 -> 61 bytes .../wasmq/refuse/data-past-memory.wat | 1 + .../wasmq/refuse/import-clock.wasm | Bin 0 -> 76 bytes .../wasmq/refuse/import-clock.wat | 1 + .../wasmq/refuse/import-random.wasm | Bin 0 -> 80 bytes .../wasmq/refuse/import-random.wat | 1 + .../wasmq/refuse/memory-too-large.wasm | Bin 0 -> 43 bytes .../wasmq/refuse/memory-too-large.wat | 1 + .../wasmq/refuse/memory-unbounded.wasm | Bin 0 -> 40 bytes .../wasmq/refuse/memory-unbounded.wat | 1 + .../wasmq/refuse/module-hash-mismatch.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/other-capability.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/prior-hash-mismatch.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/program-hash-mismatch.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/relaxed-simd.wasm | Bin 0 -> 51 bytes .../wasmq/refuse/relaxed-simd.wat | 1 + .../wasmq/refuse/reserved-export.wasm | Bin 0 -> 58 bytes .../wasmq/refuse/reserved-export.wat | 1 + .../wasmq/refuse/shared-memory.wasm | Bin 0 -> 41 bytes .../wasmq/refuse/shared-memory.wat | 1 + .../wasmq/refuse/simd.wasm | Bin 0 -> 40 bytes wai/quantum-conformance/wasmq/refuse/simd.wat | 1 + .../wasmq/refuse/start-function.wasm | Bin 0 -> 58 bytes .../wasmq/refuse/start-function.wat | 1 + .../wasmq/refuse/table-grow.wasm | Bin 0 -> 47 bytes .../wasmq/refuse/table-grow.wat | 1 + .../wasmq/refuse/table-size.wasm | Bin 0 -> 43 bytes .../wasmq/refuse/table-size.wat | 1 + .../wasmq/refuse/tail-call.wasm | Bin 0 -> 56 bytes .../wasmq/refuse/tail-call.wat | 1 + .../wasmq/refuse/trailing-byte.wai | Bin 0 -> 41455 bytes .../wasmq/refuse/truncated.wai | Bin 0 -> 41453 bytes wai/quantum-conformance/wasmq/vectors.json | 233 +++ wai/registry/capabilities.json | 14 + wai/vocab/v2.shapes.ttl | 4 + wai/vocab/v2.ttl | 12 + wai/wai-quantum-cli/Cargo.lock | 698 ++++++++- wai/wai-quantum-cli/Cargo.toml | 6 +- wai/wai-quantum-cli/docs/llms-full.txt | 125 ++ wai/wai-quantum-cli/locales/zh-CN.json | 2 + wai/wai-quantum-cli/src/main.rs | 45 +- wai/wai-quantum-cli/src/wasmq.rs | 265 ++++ wai/wai-quantum-cli/tests/wasmq.rs | 137 ++ wai/wai-quantum-qpu/bring-your-own-quantum.md | 10 + wai/wai-quantum/src/qasm.rs | 77 + wai/wai-quantum/src/qasm/elab.rs | 192 ++- wai/wai-quantum/src/qasm/lang_tests.rs | 51 + wai/wai-quantum/src/qasm/parse.rs | 33 + wai/wai-rs/src/codecs/glossary_check.rs | 1 + wai/wai-rs/src/codecs/mod.rs | 15 +- wai/wai-rs/src/codecs/registry.rs | 3 +- wai/wai-wasmq/Cargo.lock | 1342 +++++++++++++++++ wai/wai-wasmq/Cargo.toml | 47 + wai/wai-wasmq/README.md | 32 + wai/wai-wasmq/modules/.cargo/config.toml | 3 + wai/wai-wasmq/modules/Cargo.lock | 462 ++++++ wai/wai-wasmq/modules/Cargo.toml | 19 + wai/wai-wasmq/modules/atom-planner/Cargo.toml | 20 + wai/wai-wasmq/modules/atom-planner/src/lib.rs | 116 ++ wai/wai-wasmq/modules/build.sh | 80 + .../modules/qldpc-decoder/Cargo.toml | 20 + .../modules/qldpc-decoder/src/lib.rs | 123 ++ wai/wai-wasmq/src/abi.rs | 243 +++ wai/wai-wasmq/src/bin/wasmq_vectors.rs | 43 + wai/wai-wasmq/src/engine.rs | 360 +++++ wai/wai-wasmq/src/envelope.rs | 346 +++++ wai/wai-wasmq/src/interp.rs | 1059 +++++++++++++ wai/wai-wasmq/src/lib.rs | 27 + wai/wai-wasmq/src/meter.rs | 387 +++++ wai/wai-wasmq/src/profile.rs | 473 ++++++ wai/wai-wasmq/src/reference.rs | 132 ++ wai/wai-wasmq/src/run.rs | 589 ++++++++ wai/wai-wasmq/src/vectors.rs | 85 ++ wai/wai-wasmq/tests/adversarial.rs | 287 ++++ wai/wai-wasmq/tests/engines.rs | 304 ++++ wai/wai-wasmq/tests/reference.rs | 247 +++ wai/wai-wasmq/tests/vectors.rs | 158 ++ wai/wai-wasmq/tests/wit.rs | 87 ++ 107 files changed, 10097 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/wai-quantum-wasmq.yml create mode 100644 wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md create mode 100644 wai/extensions/quantum-wasmq.md create mode 100644 wai/extensions/quantum-wasmq.wit create mode 100644 wai/quantum-conformance/wasmq/.gitattributes create mode 100644 wai/quantum-conformance/wasmq/README.md create mode 100644 wai/quantum-conformance/wasmq/envelopes/atoms4x4-plan64.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/atoms9x9-plan.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-depth.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-fuel.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/gross-decode.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/nan-canonical.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json create mode 100644 wai/quantum-conformance/wasmq/envelopes/surface3-decode64.qasm create mode 100644 wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai create mode 100644 wai/quantum-conformance/wasmq/modules/atom_planner.wasm create mode 100644 wai/quantum-conformance/wasmq/modules/modules.json create mode 100644 wai/quantum-conformance/wasmq/modules/qldpc_decoder.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/atomic.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/atomic.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/data-past-memory.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/data-past-memory.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/import-clock.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/import-clock.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/import-random.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/import-random.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-too-large.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-too-large.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-unbounded.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-unbounded.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/other-capability.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/program-hash-mismatch.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/relaxed-simd.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/relaxed-simd.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/reserved-export.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/reserved-export.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/shared-memory.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/shared-memory.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/simd.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/simd.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/start-function.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/start-function.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/table-grow.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/table-grow.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/table-size.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/table-size.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/tail-call.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/tail-call.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/trailing-byte.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/truncated.wai create mode 100644 wai/quantum-conformance/wasmq/vectors.json create mode 100644 wai/wai-quantum-cli/src/wasmq.rs create mode 100644 wai/wai-quantum-cli/tests/wasmq.rs create mode 100644 wai/wai-wasmq/Cargo.lock create mode 100644 wai/wai-wasmq/Cargo.toml create mode 100644 wai/wai-wasmq/README.md create mode 100644 wai/wai-wasmq/modules/.cargo/config.toml create mode 100644 wai/wai-wasmq/modules/Cargo.lock create mode 100644 wai/wai-wasmq/modules/Cargo.toml create mode 100644 wai/wai-wasmq/modules/atom-planner/Cargo.toml create mode 100644 wai/wai-wasmq/modules/atom-planner/src/lib.rs create mode 100755 wai/wai-wasmq/modules/build.sh create mode 100644 wai/wai-wasmq/modules/qldpc-decoder/Cargo.toml create mode 100644 wai/wai-wasmq/modules/qldpc-decoder/src/lib.rs create mode 100644 wai/wai-wasmq/src/abi.rs create mode 100644 wai/wai-wasmq/src/bin/wasmq_vectors.rs create mode 100644 wai/wai-wasmq/src/engine.rs create mode 100644 wai/wai-wasmq/src/envelope.rs create mode 100644 wai/wai-wasmq/src/interp.rs create mode 100644 wai/wai-wasmq/src/lib.rs create mode 100644 wai/wai-wasmq/src/meter.rs create mode 100644 wai/wai-wasmq/src/profile.rs create mode 100644 wai/wai-wasmq/src/reference.rs create mode 100644 wai/wai-wasmq/src/run.rs create mode 100644 wai/wai-wasmq/src/vectors.rs create mode 100644 wai/wai-wasmq/tests/adversarial.rs create mode 100644 wai/wai-wasmq/tests/engines.rs create mode 100644 wai/wai-wasmq/tests/reference.rs create mode 100644 wai/wai-wasmq/tests/vectors.rs create mode 100644 wai/wai-wasmq/tests/wit.rs diff --git a/.github/workflows/wai-quantum-wasmq.yml b/.github/workflows/wai-quantum-wasmq.yml new file mode 100644 index 000000000..54aa1ead1 --- /dev/null +++ b/.github/workflows/wai-quantum-wasmq.yml @@ -0,0 +1,202 @@ +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/src/qasm.rs' + - 'wai/wai-quantum/src/qasm/**' + - 'wai/wai-quantum/src/engine/dynamic.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' + - '.github/workflows/wai-quantum-wasmq.yml' + pull_request: + paths: + - 'wai/wai-wasmq/**' + - 'wai/quantum-conformance/wasmq/**' + - 'wai/extensions/quantum-wasmq.*' + - 'wai/wai-quantum/src/qasm.rs' + - 'wai/wai-quantum/src/qasm/**' + - 'wai/wai-quantum/src/engine/dynamic.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' + - '.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" + n=$(grep -c '^test result: ok\.' "$RUNNER_TEMP/wasmq.log" || true) + [ "$n" -ge 7 ] || { echo "::error::$n test targets passed; the crate has 7 (the library, the binary, engines, reference, adversarial, vectors, wit)"; exit 1; } + if grep -q 'FAILED\|test result: FAILED' "$RUNNER_TEMP/wasmq.log"; then echo "::error::a test failed"; exit 1; fi + for t in every_instruction_gives_the_same_value_or_trap_on_both_engines \ + the_fuel_is_the_same_on_every_engine_and_is_the_native_engines_own_count \ + a_run_that_runs_out_stops_at_the_same_charge_everywhere \ + the_call_depth_is_bounded_the_same_everywhere \ + the_deepest_frames_the_profile_allows_fit_every_engine \ + the_decoder_module_decodes_as_the_engine_does_on_every_engine \ + the_planner_module_plans_as_the_engine_does_on_every_engine \ + a_memory_round_decodes_in_the_middle_of_the_circuit_and_every_shot_ends_in_the_code \ + atoms_are_rearranged_in_the_middle_of_the_circuit_and_fill_the_target \ + without_canonicalisation_a_nan_path_reads_what_the_host_chose \ + a_callout_past_its_fuel_ends_in_a_signed_fault_receipt \ + a_prior_whose_hash_is_not_the_pinned_one_is_refused \ + the_vectors_hold_on_every_engine_of_this_build \ + the_reference_modules_export_what_their_worlds_lower_to; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/wasmq.log" || { echo "::error::$t did not run and pass"; exit 1; } + done + - 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 + + 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 --test engines --test reference --test adversarial --test vectors --test wit 2>&1 | tee "$RUNNER_TEMP/wasip2.log" + if grep -q 'FAILED\|test result: FAILED' "$RUNNER_TEMP/wasip2.log"; then echo "::error::a test failed under wasip2"; exit 1; fi + n=$(grep -c '^test result: ok\.' "$RUNNER_TEMP/wasip2.log" || true) + [ "$n" -ge 6 ] || { echo "::error::$n test targets passed under wasip2; 6 run there"; exit 1; } + - 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-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/wai/REGISTRY.md b/wai/REGISTRY.md index ac083b7d8..eaa55efc0 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -288,6 +288,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 | diff --git a/wai/SPEC.md b/wai/SPEC.md index c850e97bb..71fdc74d2 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -938,6 +938,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, 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): every conforming runner gives the same result bytes, the same fuel for every callout and the same fault, on every host and engine; each module held to a deterministic profile (Lime1, nothing imported, memory bounded, NaNs canonical, fuel and call depth counted inside the module) and the run sealed in the job receipt. The quantum part is simulated; on hardware the classical part replays from a record of its calls (§7 there) | *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..84c96cf30 --- /dev/null +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -0,0 +1,40 @@ +### 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). +- **The profile.** A module is a core module in the Lime1 feature set that imports nothing, + bounds its memory, and has no start function, threads, vectors, references or growing + table; each rule has a refusal code (`W001`–`W017`). WebAssembly 3.0's deterministic profile + leaves `memory.grow`, `table.grow` and the call depth to the host; the profile fixes all + three. +- **The same bytes and fuel everywhere.** A module is prepared: rewritten, keeping every + instruction, so that it charges its own fuel at the start of each run of instructions, + counts its own calls to a depth of 1024, and returns only canonical NaNs. The fuel is the + pinned wasmtime's own default count, which a test holds equal on every kind of control + flow, call and memory instruction. Two engines run every module, the pinned wasmtime and + an interpreter of the crate's own that also runs on wasm32, and every instruction, fuel + charge, trap and fault agrees between them. +- **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' and prepared + modules' digests, a digest of every call's inputs and outputs, and the fault that stopped + it (fuel, call depth, a trap or a result past its width), as a signed fault receipt. +- **The command line**: `wai-quantum pack-wasmq`, `check-wasmq`, `run ENVELOPE.wai` + (`--engine`, `--transcript`, `--replay`), and `verify JOB --bundle ENVELOPE.wai`, which runs + the envelope again and compares byte for byte. 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. +- **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, with their codes. diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index 671626d3b..48e042090 100644 --- a/wai/extensions/quantum-providers.md +++ b/wai/extensions/quantum-providers.md @@ -314,6 +314,10 @@ other counts. The `ideal` distance says how far they are from what the circuit gives; a distance well above the sampling distance is the device's error, or a program the device did not run as sent. +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: diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md new file mode 100644 index 000000000..62a4c4d09 --- /dev/null +++ b/wai/extensions/quantum-wasmq.md @@ -0,0 +1,506 @@ +# 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`). 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, plans the moves that fill an atom array, +updates a parameter from what it measured. 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 is bounded, and which uses nothing whose result a host + may choose (§3). +- **The same bytes and the same fuel everywhere.** A module is *prepared* (§4): rewritten + so that it meters its own fuel, counts its own calls and returns only canonical NaNs. + Any WebAssembly engine then runs it with the same results, the same fuel and the same + fault. Two engines run it in the reference, the pinned native engine 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). + +A run here simulates the quantum program and runs the classical code for real. §7 says +what holds when the quantum part runs on hardware. + +## 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). + +## 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 qubits the program declares, 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` | for each `extern` the program declares: `{ "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 the method the program runs by: +`stabilizer` when every gate is a Clifford gate, `statevector` otherwise, for at most 24 +qubits. `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 each budget +are at most 2^53 − 1, so that every JSON reader holds them exactly. + +### 2.3 Reading an envelope + +A reader MUST refuse an envelope, with the code of §3.3, when: + +- 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), the program is refused by the reader or calls + nothing (`W016`), a callout does not match its module's export (§5.2, `W015`), or the + run is outside §2.2's ranges (`W017`). + +### 2.4 One envelope for one bundle + +A writer MUST write the frames in §2.2's order, the contract in its canonical form 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. 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, 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). | +| No shared memory, no atomic instruction. | Lime1 has no threads; a shared memory is refused by name. | +| No vector instruction, relaxed or not. | A relaxed vector instruction's result is the engine's choice; the profile takes none. | +| No reference, exception, tail-call or garbage-collection instruction or type. | 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 locals and operands at once. | It bounds what one frame takes, so the call depth (§4.3) bounds the stack on every engine. | +| At most 16 MiB of bytes and 2^20 functions. | | +| Every active data and element segment fits where it is placed. | Instantiation then cannot fail. | +| No export begins `__wasmq`. | The prepared module's names (§4.4). | + +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 two rules (§4.5) and closes the others: the +table never grows, the memory grows only within the maximum the module declares (§4.7), +and the call depth is counted (§4.3). + +### 3.3 Refusal codes + +| code | the module or envelope | +|---|---| +| `W001` | is not a valid core module in Lime1 (a component, a malformed module, an instruction no other rule names) | +| `W002` | imports something | +| `W003` | has more than one memory, a shared or 64-bit one, one without a maximum or past 4096 pages | +| `W004` | has a start function | +| `W005` | has a shared memory or an atomic instruction | +| `W006` | has a vector instruction, relaxed or not | +| `W007` | has a reference, exception, tail-call or garbage-collection instruction or type | +| `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 locals and operands can pass 4096 | +| `W010` | is larger than 16 MiB or has more than 2^20 functions | +| `W011` | has a data or element segment that does not fit where it is placed | +| `W012` | exports a name the profile reserves, or a lifecycle export (§5.3) with the wrong type | +| `W013` | (envelope) is not one this profile writes (§2.3) | +| `W014` | (envelope) has a frame whose digest is not the one its contract pins | +| `W015` | (envelope) has a callout whose module does not export it with the type its `extern` lowers to | +| `W016` | (envelope) has a program the reader refuses, that calls nothing, or that is too large for its method | +| `W017` | (envelope) has a run outside the profile's ranges | + +`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 into an instance is given a budget 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, the page count it takes is + charged, whether or not the growth succeeds. +- 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. These are the pinned native engine's default operator costs, with one for +a byte of `memory.copy` or `memory.fill` and one for a page of `memory.grow`; a test holds +the profile's fuel equal to that engine's own count, on the original module, over every +kind of control flow, call and memory instruction (`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 Call depth + +A host call starts at depth 0. Each `call` and `call_indirect` raises the depth by one +before it calls, and lowers it by one when the callee returns. A call that would raise it +past **1024** stops with `call_depth_exhausted`. With the frame rule of §3.2, a call within +the depth holds at most 1024 frames of at most 4096 slots on any engine, which the +reference engines hold; so no conforming module meets an engine's own stack limit. + +### 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). Any other engine runs the **prepared module**, a pure +function of the module's bytes that keeps every instruction byte for byte and adds: + +1. two mutable globals, after the module's own, exported as `__wasmq_fuel` (i64) and + `__wasmq_depth` (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 the page count, each taken from the operand with `local.tee` of one + added `i32` local; +4. around each `call` and `call_indirect`, + `global.get D; i32.const 1; i32.add; global.set D; global.get D; i32.const 1024; i32.gt_u; if; unreachable; end` + before it and `global.get D; i32.const 1; i32.sub; global.set D` 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; + +and drops every custom section. The host sets `__wasmq_fuel` to the call's budget and +`__wasmq_depth` to 0 before each call. When the prepared module traps at `unreachable` +with `__wasmq_fuel` below zero, the fault is `fuel_exhausted`; with `__wasmq_depth` past +1024, `call_depth_exhausted`; otherwise the module's own `unreachable`. + +Because the preparation meters, counts and canonicalises inside the module, any +conforming WebAssembly engine runs a prepared module with the same results and fuel. The +prepared module is itself a Lime1 module; its digest is in the receipt (§6.4). + +### 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, threads, tail calls and every proposal past Lime1 + off, and its own fuel on as a backstop under the profile's; +- **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 runs the prepared module too, with its own metering off. + +`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. + +## 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); 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. + +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 host allocates + each argument with the module's `cabi_realloc(0, 0, 1, len)`, writes it, calls, reads + the result, and calls `cabi_post_(retptr)` if the module exports it: the + Component Model's canonical ABI. + +The host charges the call one fuel for each byte it writes or reads (§4.2). A result +whose bits pass its width, a list result of the wrong length, outside the memory or +not 4-aligned, ends the run with `bad_result`. Every guest function the host calls for a +callout (`cabi_realloc`, the export, `cabi_post_`) shares the callout's budget. + +### 5.3 Lifecycle + +When an instance is made (each shot when `state` is `shot`, once when it is `job`), the +host calls, in this order and each if the module exports it, each under the instance +budget `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; +3. `init()`. + +A module with priors MUST export `wasmq-prior`. + +### 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 randomness + +A run goes shot by shot, in order. Each shot starts from `|0…0⟩` with every bit 0. A +measurement of a qubit whose outcome is 1 with probability `p` reads 1 when +`((x >> 11) + 0.5) / 2^53 < p`, with `x` the next value of SplitMix64 seeded with the +shot's seed XOR `0x51574d31`; the shot's seed is SplitMix64's output for the state +`seed + (shot + 1) · 0x9e3779b97f4a7c15`. A shot's outcome is its program bits, bit +`n − 1` first, and the counts sum the shots that read each outcome. + +### 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: `call`, `instance` or `run` | +| `call_depth_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 | + +with the shot, the phase (`seed`, `prior`, `init` or `call`), the module and, for a call, +the callout, and the fuel the stopped call used. 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. + +### 6.4 The result + +The job body is the engine's (`engine::job`): `{ "status", "method", "request", "result" }`, +`status` `done` or `fault`. The request is `{ "wasmq": , "envelope_blake3" }`. +The result: + +| member | | +|---|---| +| `format` | `"wai.quantum.wasmq.result/1"` | +| `profile`, `status`, `method`, `qubits`, `shots`, `shots_run`, `seed` | | +| `counts` | outcome to shots | +| `callouts` | calls made | +| `fuel` | `{ "used", "instances", "budget", "callouts": { name: { "calls", "fuel", "max" } } }` | +| `modules` | for each: `{ "name", "blake3", "prepared_blake3", "memory_pages", "floats" }` | +| `transcript_blake3` | BLAKE3 over `wai:wasmq-transcript\x01` and, for each call in order, its shot (u64), callout index (u32), input length (u32) and bytes, output length (u32) and bytes, and fuel (u64), little-endian; after a fault, `fault`, the shot, the reason and the fuel | +| `fault` | §6.2, when the run stopped | +| `accuracy` | `{ "class": "exact", "note" }` | +| `energy` | `{ "unmetered": … }`: the runner reads no energy meter; fuel is the measure of work | +| `receipt` | the engine's job receipt (`engine::receipt`): Ed25519 over BLAKE3 of the request and the result in RFC 8785 form | + +Nothing in the result depends on the host or the engine, so the result's bytes, and the +receipt's digest, are the same everywhere; with the same key, the whole job is. + +### 6.5 Verifying + +`wai-quantum verify JOB.json` checks the signature, that the result is one this profile +writes (fuel within the contract's budgets, each callout's largest call within the call +budget, the modules the contract pins, a fault reason §6.2 names, counts that sum to the +shots run), and with `--bundle ENVELOPE.wai` that the envelope is the one the receipt +binds and that running it again here gives the same result, byte for byte. + +## 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 cannot 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. + +**No provider's machine runs a wasmQ module in its control loop today.** Real-time +classical compute beside a quantum processor exists: one vendor's control system runs +WebAssembly modules on hardware modules next to its processor, called from its programs +through its own interface, with integer arguments on one system and integers and doubles +on another; another runs native functions on co-located accelerators over a network +transport, with a timeout. Neither, in what they publish, fixes a deterministic profile, +meters the callout, binds the binary by digest or signs what ran. + +What holds for a module on such a machine, and what does not: + +- A scalar-form module (§5.2) is a plain integer function with no imports, the shape + those WebAssembly environments take. Whether a given machine accepts it is that + machine's to say; this repository has not run a module on one. +- 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`). 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 stabilizer 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; +- `refuse/`: modules and envelopes a runner MUST refuse, each with its code + (`vectors.json`, `refuse`), the modules also as WebAssembly text; +- `modules/`: the modules of record (§8). + +`wasmq_vectors verify wai/quantum-conformance/wasmq` checks them on every engine of the +build and prints a digest of the outcome, 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 lowering, the transcript, the result's +members) is a new profile version, and a receipt names the version it was made under. diff --git a/wai/extensions/quantum-wasmq.wit b/wai/extensions/quantum-wasmq.wit new file mode 100644 index 000000000..efccd4a5c --- /dev/null +++ b/wai/extensions/quantum-wasmq.wit @@ -0,0 +1,60 @@ +// 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. +// +// 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, + call-depth-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 a05beafd9..bd381b493 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/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..8d17c0070 --- /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 in +that file. 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 and its sealed job | +| `refuse/` | modules (also as WebAssembly text) and envelopes a runner refuses; `vectors.json` `refuse` gives each one's code | + +```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 \ + 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 0000000000000000000000000000000000000000..794e1e44ab911117fb6e0e4a27f98ea01098db4f GIT binary patch literal 25364 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 Vx0jE4l^#l&=_g^6!pjxi{{qJMom&6^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f0b9ac174bd2fda8b572b74b69bae58e7ba96052 GIT binary patch literal 25376 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 Pf8=1#FFM!@NML^f+Qb2D literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea26fd735d7575267065827953cfad05751b816a GIT binary patch literal 860 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 PakzQmeS7)S|NP(=i$nt- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..03b082bcf54b04e27c120b4f5549c723d2532bac GIT binary patch literal 857 zcmZWn&2H2%5Kf8!DO?ff$S07NC3cc-c2^)3!GS}8ibD@oRcvRnHBFqX8-Yn>~rYOVB&EP8UHbUwy9KKd;;E5O=4S$9I&mMpRfX^-ox7XZ@PFnrqby#2jh z`onkaNGwx#j&slxIBRO-M zGnR5nBbrc%VSAQD+dP`3RBkhtiWw6iBA{uSq?EHFl1ZG*T}5$jUDlLNh~^bmBNd{J zQMGZ!4qffW)P-_N{vX;SI%{Z`rz^R)KN7R#|gVOyQFh_2rWI-{B`%p4s zO1GA4iMp@wOUJuf{DrvyOj?0GuHAAm$Q{XhFsPy3Z1A6@T6bc|!!?s(c=h`E%e|d9 zyJ0+ymVwf;>~aV(OTI#0J4J*ngUw>Y;yzxHbj0pnn literal 0 HcmV?d00001 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..c2def3df0 --- /dev/null +++ b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json @@ -0,0 +1,117 @@ +{ + "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": 1000000000000 + }, + "method": "stabilizer", + "seed": 1234, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "f442722b7b2bbf0d3674f186cd86137f2a63110f20db73a519993c90d5dc67a2" + }, + "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": 803644, + "instances": 215296, + "budget": { + "per_call": 10000000, + "per_job": 1000000000000, + "per_instance": 100000000 + }, + "callouts": { + "decode64": { + "calls": 32, + "fuel": 588348, + "max": 21751 + } + } + }, + "modules": [ + { + "name": "qldpc-decoder", + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962", + "prepared_blake3": "20b25f014aad7adae6257af37b1350eddc06ff9de09d87578a8e13d9afd93e61", + "memory_pages": [ + 17, + 1024 + ], + "floats": false + } + ], + "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", + "accuracy": { + "class": "exact", + "note": "each shot runs alone, its measurements drawn from the run's seed and the shot's number; each callout runs in a metered module with the same results, fuel and fault on every host" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", + "digest": "2013608fef63aa4f46f50e8656fd35e1bbe673b18e6a18ffb2dbbf315332de06", + "signature": "1e5faffea8155469e251891e1a846ad178225b1ec96fac1aa49627bb34bd842570aa688cbea54abb54744c01158b65ef36d44ad4688baff830f141ad491e4104" + } + } +} 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 0000000000000000000000000000000000000000..96525914fcb2e373efaac7f7c861b37063ae5604 GIT binary patch literal 41454 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 E0BBe}VE_OC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..74188cb3facab3960e2e6595c185a46774ad15fc GIT binary patch literal 43836 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be55193f3e0fe97cc7ff7689e34d1c5bc19e2a48 GIT binary patch literal 24165 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 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/modules/modules.json b/wai/quantum-conformance/wasmq/modules/modules.json new file mode 100644 index 000000000..29c35e8c2 --- /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": "8afe4fdcfcfe2de71e9f81376868686c67de2d21c82656e8a83dc827d60ba950", + "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 0000000000000000000000000000000000000000..bc5f14cf9416e20babb007e15ccc5f936ef98f8c GIT binary patch literal 39442 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 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/refuse/atomic.wasm b/wai/quantum-conformance/wasmq/refuse/atomic.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0dec7b4b6752a44a23cb4392c7e806a840f2de94 GIT binary patch literal 47 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=tVFUtpRz}7&1_mx3MlJ?N1_g$H^2`j}0DNHt A-2eap literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/refuse/atomic.wat b/wai/quantum-conformance/wasmq/refuse/atomic.wat new file mode 100644 index 000000000..edec2fef6 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/atomic.wat @@ -0,0 +1 @@ +(module (memory 1 1) (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/contract-run-changed.wai b/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai new file mode 100644 index 0000000000000000000000000000000000000000..f5f3cae354ff9f9ecff7316d90c498e9486aa758 GIT binary patch literal 41454 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 HRQ~?}36woz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d66ede0464f813bea3e6d30c22316676d22b5752 GIT binary patch literal 61 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=tVFUtpRz}7&1_mxRMivGI25xR4Mh3@U|CzbD O3=EBoO-#+qEi3`ksR!)< literal 0 HcmV?d00001 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/import-clock.wasm b/wai/quantum-conformance/wasmq/refuse/import-clock.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4acb8aef68eb1458193e03a161318aa766e671ab GIT binary patch literal 76 zcmZQbEY4+QU|?Y6VoG3OtV>|5W2|FRW|S^ZEY7q_&dE>CF4oV@&&w~#&&y2K1q(3e b<(D%sFf%bSva>QWrUA9GF|sfyFmM9^yd4mX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bb8dd60fa7ee9fb28a73cf091474ef3a011b5106 GIT binary patch literal 80 zcmV~$ISzmz6a>(j@0O^wwT%bxMEDa?DO@5%I=H&;LH$Vp>}8RhnHMG|nK(rC&iG3o gf84=5ul<~jbNlMG-ly4Z016?Sirm1qW{DBq4@OiH$p8QV literal 0 HcmV?d00001 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/memory-too-large.wasm b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wasm new file mode 100644 index 0000000000000000000000000000000000000000..98fa2d7729b8672362187a7cb4ef188b627dc4d5 GIT binary patch literal 43 xcmV~$u?>JQ3;@A@;uwi6krnbnbZ8Ct!NJ-R+Q`YE_(FU%1j>GJbbK1a$xa literal 0 HcmV?d00001 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..aa606182f --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat @@ -0,0 +1 @@ +(module (memory 1 65536) (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 0000000000000000000000000000000000000000..91e2977553bc27dd7671426845f84e66f19e0bbf GIT binary patch literal 40 scmV~$xe)*m006-|?+i^SjYKArtKBZ^9s!Hf=Zisd6l>NEt(X|}4_yEQZ2$lO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ffcf82715e4fe23f8baa95c00fd96397016d9668 GIT binary patch literal 41454 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 E1Jcqxp#T5? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9bf5873f70f17755222181514e689320098eeff4 GIT binary patch literal 41454 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 I)}`|Q2h$WiXaE2J literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..993407835e98979e2e6e775af788bdfeca5db601 GIT binary patch literal 41454 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 EA83?4VgLXD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3621643c7bc3424ead8027aad3f481f7b06ef3ab GIT binary patch literal 41454 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 E07KqAaR2}S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7177b2cb9d213b6ce13987e109f9cb688e62ecea GIT binary patch literal 51 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=vWn@faVBivC6k$+c_$vg%e_NUU$}(^R0Kvxz AjsO4v literal 0 HcmV?d00001 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..63b523eb5 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/relaxed-simd.wat @@ -0,0 +1 @@ +(module (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 0000000000000000000000000000000000000000..60cb2a6a1c70f25bb7023a044099e3fefde4f407 GIT binary patch literal 58 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=r1G1bLxYC$;7#O(N K7+Dw;7`Oov%n3~Z literal 0 HcmV?d00001 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/shared-memory.wasm b/wai/quantum-conformance/wasmq/refuse/shared-memory.wasm new file mode 100644 index 0000000000000000000000000000000000000000..380b09e57e6795733d0cc0f3dc87d5511cb14163 GIT binary patch literal 41 tcmV~$xe)*m006-|?+i^SjYKArtKBZ^9s!Hf=ZisdGHT7bp%oK@{sCYU19AWW literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..94907dbb5868f110997be1629d6b4358184c9772 GIT binary patch literal 40 vcmZQbEY4+QU|?WmV@zPIW2|FlVq{=vWn@faVBq3rib1`hxL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd3470bf8e78271a00b5848cb41e92ff14683f2d GIT binary patch literal 47 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=pVJu)^WM^e$Ok-f+;$h@sxKQB8_=lfiEjIv> C5(fhS literal 0 HcmV?d00001 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..624a3974c --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/table-grow.wat @@ -0,0 +1 @@ +(module (table 1 funcref) (func (export "f") (param i64) (result i64) ref.null func i32.const 1 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 0000000000000000000000000000000000000000..dd8244daf90a731d4cd54e00a21e3cc97d6c96db GIT binary patch literal 43 ycmZQbEY4+QU|?WmV@zPIW2|FlVq{=pVJu)^WM^e$Ok-f+;$UQB_#?oumKy+dK?QmM literal 0 HcmV?d00001 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..8b9b52069 --- /dev/null +++ b/wai/quantum-conformance/wasmq/refuse/table-size.wat @@ -0,0 +1 @@ +(module (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 0000000000000000000000000000000000000000..4835c958ed9ed8ce834e602c6177dce7edca78f1 GIT binary patch literal 56 zcmWN>F%m#96hOiEl8709G)~}XQlN@PpR=>Y(%vp$^>8?HPB2e;;j*T}PSn4RAj;GF HX%X)astX0r literal 0 HcmV?d00001 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..c604c258e --- /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 (export "f") (param i64) (result i64) local.get 0 return_call $g)) 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 0000000000000000000000000000000000000000..cdbed5fd5d0abae59202cc5bf33e4120c9c540b9 GIT binary patch literal 41455 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 EAEmiHVE_OC literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/refuse/truncated.wai b/wai/quantum-conformance/wasmq/refuse/truncated.wai new file mode 100644 index 0000000000000000000000000000000000000000..216de6049ad5fe0ee273dc409c87405665e77ee3 GIT binary patch literal 41453 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 DD)&8M literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/vectors.json b/wai/quantum-conformance/wasmq/vectors.json new file mode 100644 index 000000000..d4dc9f7d3 --- /dev/null +++ b/wai/quantum-conformance/wasmq/vectors.json @@ -0,0 +1,233 @@ +{ + "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": "f442722b7b2bbf0d3674f186cd86137f2a63110f20db73a519993c90d5dc67a2", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 803644, + "callouts": 32, + "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", + "result_blake3": "ffa992ceedd34a0909f18e77d113802c58436290f5e56f2d4080b92a6519acb6", + "receipt_digest": "2013608fef63aa4f46f50e8656fd35e1bbe673b18e6a18ffb2dbbf315332de06", + "job_blake3": "9ab75fcafd0238ca4993694ae6621dea43cc7af1ce79f1f68554d393ccfa910a" + } + }, + { + "file": "envelopes/surface5-decode64-job.wai", + "envelope_blake3": "3f74b00339613f7a03ca546a311e23a4e75f96281ecb15cbe1baa484421935c2", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 764587, + "callouts": 16, + "transcript_blake3": "eb5252076ea8bb4c45ccec714a3cc64b89e833fe0a49c36a28881b63b1c3799e", + "result_blake3": "167c7a3de201749704b6663f9be20200259b2e241eba67cba29e0d6790aaff92", + "receipt_digest": "df8996dfd9e630e5cc0b0de7a7f83370bada3f472cfb4561801046efd940b577", + "job_blake3": "6c98553e45efd97f6ee87d2fca8e04bac44e352efd2c9baf375baf6fd9576459" + } + }, + { + "file": "envelopes/gross-decode.wai", + "envelope_blake3": "f7d77db307d341f469ef3d365a1d1503e9b20c49d750bce4c9df56fc93e52743", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 1017820, + "callouts": 4, + "transcript_blake3": "29b7d81e2ebc5d2dac6413ab3c15a2da2b2dcac03f3c68633a10bde60e59d3c4", + "result_blake3": "22104a329bc5ac7072b20892c88140a299b04b25b1f4b0938cd8c94c118b0965", + "receipt_digest": "3ae5fe8c5059570dfb0688225c6e3fdf14ad6d6123ac1802113bc30fd02c4558", + "job_blake3": "c4638a88f5a142559bfe3d8d30e9ff9d679fd4fc48baccc24496f401a9638836" + } + }, + { + "file": "envelopes/atoms4x4-plan64.wai", + "envelope_blake3": "01c344f03ed95fc91145e592e6f664d79c8693e3b1f1fc67b05245b82e8c7ca6", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 543607, + "callouts": 32, + "transcript_blake3": "927caf8a41a7e9a654471532af1f428ddc86cb5efab5b0d64ba21fae2726d752", + "result_blake3": "3ade9928b900703bb3790b63224f51a2a18c428d4383cf1e0a0cf01ed15ee78e", + "receipt_digest": "ef6f3e94334504d959a8535c1e775e89f9e44f47e0b5374e60aafa08e096a79a", + "job_blake3": "faac019f247b90d9b851dc4934fab93641c06cdfc2714fe7365a115dfd3779a9" + } + }, + { + "file": "envelopes/atoms9x9-plan.wai", + "envelope_blake3": "c305f88b785110e9313ab6385cbbf2f03d8cf9596ff164f8a7c9bd5f16da4f32", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 908639, + "callouts": 6, + "transcript_blake3": "e280bbc8ba046e7f003d173c4c280050d63e1b972ec216c465ce67bed621ed6a", + "result_blake3": "a8e6dfa99f8d33b426e6ec17f13a6a87ad4d372d5246cb94162b363bf5f7452b", + "receipt_digest": "ba1a4f9244ffa4bc1b1c8e556af6cbf5fb4b3c6fed850e85c42fe2ad6c3dbae7", + "job_blake3": "35e5d6974fc8c1df1d1b39dc198b22d4bbd1020b5ac30fa3ff6c35762ec327d5" + } + }, + { + "file": "envelopes/fault-fuel.wai", + "envelope_blake3": "b1cb5f210f7e365281d534ea91b371fdbb497dcd9f8cdce927a6e4a7a998b7cc", + "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": "b27614fada70a4a7cbd9880fd74ba18e3035eb265d29493553c933dda3a0d594", + "receipt_digest": "d6c9da88bf04c6219b619cf314e08230f6d1690cf69bafcb07efc049ee4004a4", + "job_blake3": "356eedbeb8258019393ca87de091094c864c281a4698761daa3f4bf034129257" + } + }, + { + "file": "envelopes/fault-depth.wai", + "envelope_blake3": "b1e71eb5c12fe22abf72d917bf9e62985fcb2d6d77621fc05831a334e51b356e", + "expect": { + "status": "fault", + "fault": { + "reason": "call_depth_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 5123, + "callout": "f" + }, + "fuel_used": 5123, + "callouts": 1, + "transcript_blake3": "98cea88af5c5871a322a59c46d14c8e0ab651509e90099b963ebe42f47d116d7", + "result_blake3": "1f0db882ef25704f91e1789b6de880f1a3e4445454eeb2a8b87c162a4fb6bda8", + "receipt_digest": "f629a275551e82489aad100235aa8f4cab421fe4c9791fe4a97fadbfa66e0b21", + "job_blake3": "aa070129010f5a51dd41b1723378408410d9d4a4744a8ea03b2ebd4b206bc084" + } + }, + { + "file": "envelopes/nan-canonical.wai", + "envelope_blake3": "bccef76fd3eb96f0ade40cd19c490414b1f86c0f1a2f125247e5045a73c6a54c", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 64, + "callouts": 8, + "transcript_blake3": "d062b788107d413845ef253a9d9265548346393a28faeacfe3ef3be471ebfe7b", + "result_blake3": "c46debc74326da7925b4bc596694bf03395791b5334d7141aa72bfef1af0240c", + "receipt_digest": "a4fcaa17fc633ca3006b1d4213cd6b5e6b99193091ba29378f1907ae114ce8cb", + "job_blake3": "1f8c6744e1174d12179fadfeb516d70becdb8da1cf700190c6b968a9febcf2b5" + } + }, + { + "file": "envelopes/grow-past-maximum.wai", + "envelope_blake3": "bd7614a64c8b5c25fa86b0c56ef10c62b22edf60bf8c1ddb94eb68d7ca5a18fd", + "expect": { + "status": "ok", + "fault": null, + "fuel_used": 217, + "callouts": 16, + "transcript_blake3": "9a6988078f783b1cda95640067c315d4747c4bd4c33709c0afe72b9afe8e03d0", + "result_blake3": "a13c7df0e8b5adfd5f896ef0e33e1b2ddad81e5b7ac214035515c114b9152ef3", + "receipt_digest": "bed59af9215698f3b3b8231a7f1823b377a74c2d768debcfa66f3acd7e0b62dd", + "job_blake3": "14a1096a07efafe236da587a3c83d1c16a71019c7060a575b94403f168bd1534" + } + } + ], + "refuse": [ + { + "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/table-grow.wasm", + "code": "W007" + }, + { + "file": "refuse/table-size.wasm", + "code": "W008" + }, + { + "file": "refuse/data-past-memory.wasm", + "code": "W011" + }, + { + "file": "refuse/reserved-export.wasm", + "code": "W012" + }, + { + "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/trailing-byte.wai", + "code": "W013" + }, + { + "file": "refuse/truncated.wai", + "code": "W013" + }, + { + "file": "refuse/other-capability.wai", + "code": "W013" + } + ] +} diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 0b606c2e3..e36ad9e7d 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1866,6 +1866,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", diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index 6cb32cefc..f94dbd997 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -81,6 +81,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.set wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -170,6 +171,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.set wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -278,6 +280,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.set wai:wai.quantum.circuit wai:wai.quantum.circuit2 + wai:wai.quantum.wasmq wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -367,6 +370,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.set 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 dbf7b5704..9bb8faa7d 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -1340,6 +1340,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 dddf3bf30..a9d551434 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]] @@ -1248,12 +1718,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" @@ -1279,6 +1962,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 3117b31c1..c914d690e 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.8.1", path = "../wai-quantum", default-features = false, features = ["full"] } wai-quantum-qpu = { version = "0.4.1", path = "../wai-quantum-qpu", optional = true } +wai-wasmq = { version = "0.1.0", path = "../wai-wasmq", optional = true } diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 4ab3f937f..aba77d0af 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -1575,6 +1575,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 @@ -1867,6 +1871,124 @@ 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) + + --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 + + [default: 1000000000000] + + --fuel-per-instance + The most fuel an instance's seed, priors and init may use + + [default: 100000000] + + --input + A value for the program's integer `input` (repeatable) + + -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 @@ -3574,6 +3696,9 @@ 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 + --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 LANG names); results, codes and the engine's own messages stay in English diff --git a/wai/wai-quantum-cli/locales/zh-CN.json b/wai/wai-quantum-cli/locales/zh-CN.json index 7c49d45e9..b6e71266e 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/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 1e03e7ee4..bdbeafc40 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -114,6 +114,8 @@ mod taste; mod variational; #[cfg(feature = "service")] mod service; +#[cfg(feature = "wasmq")] +mod wasmq; #[cfg(feature = "service")] mod task; @@ -158,6 +160,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 { @@ -309,6 +326,9 @@ 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, }, /// Show the key this user signs receipts with Key { @@ -1174,7 +1194,21 @@ fn main() { let _ = FROM.set(cli.from); match cli.command { #[cfg(feature = "providers")] - Command::Run { job, out, key, provider } if provider.provider.is_some() => providers::run(&job, &out, &key, &provider), + #[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, seed and method: give them to `pack-wasmq`"); + } + 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), + #[cfg(feature = "wasmq")] + Command::CheckWasmq { module, json } => wasmq::check(&module, json), + 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); @@ -1385,9 +1419,16 @@ fn main() { } #[cfg(feature = "service")] Command::Task { cmd, args } => task::main(cmd, &args), - Command::Verify { file, history: hist } => { + Command::Verify { file, history: hist, bundle } => { let src = read_text(file.as_deref()).unwrap_or_else(|e| die(&e)); let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}"))); + #[cfg(feature = "wasmq")] + if v["result"]["format"] == wai_wasmq::run::FORMAT { + std::process::exit(if wasmq::verify(&v, bundle.as_deref()) { 0 } else { 1 }); + } + if bundle.is_some() { + die("--bundle is for a wasmQ job (`run ENVELOPE.wai --json`)"); + } if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { if !estimate::verify(&v) { std::process::exit(1); diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs new file mode 100644 index 000000000..5b3317020 --- /dev/null +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -0,0 +1,265 @@ +//! 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()))) +} + +/// Why a bundle goes to no provider. +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: no provider runs it, so it runs here. A provider that runs WebAssembly beside its processor would need the profile's engine in its control loop; none does today"; + +/// 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 + #[arg(long, default_value_t = 1_000_000_000_000)] + 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, + /// A value for the program's integer `input` (repeatable) + #[arg(long = "input", value_name = "NAME=N")] + pub inputs: Vec, + /// Where to write the envelope + #[arg(short, long, value_name = "FILE")] + pub out: PathBuf, +} + +#[derive(Args, Clone)] +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, +} + +fn module_name(p: &Path) -> String { + p.file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string() +} + +pub fn pack(a: &PackArgs) -> ! { + let program = String::from_utf8(read(&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(&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(Path::new(prior)); + modules[module].1.priors.push((module_name(Path::new(prior)), bytes)); + } + let mut inputs = wai_quantum::qasm::Inputs::new(); + for i in &a.inputs { + let (k, v) = i.split_once('=').unwrap_or_else(|| die(&format!("--input {i}: give NAME=N"))); + inputs.insert(k.to_string(), v.parse().unwrap_or_else(|_| die(&format!("--input {i}: not an integer")))); + } + 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 bundle = Bundle { program, inputs, modules: modules.into_iter().map(|(_, m)| m).collect(), callouts, run }; + let env = wai_wasmq::pack(&bundle).unwrap_or_else(|r| die(&r.to_string())); + std::fs::write(&a.out, &env).unwrap_or_else(|e| die(&format!("{}: {e}", a.out.display()))); + let o = wai_wasmq::open(&env).unwrap_or_else(|r| die(&r.to_string())); + let (h, method) = wai_wasmq::envelope::read(&bundle.program, &bundle.inputs).unwrap_or_else(|r| die(&r.to_string())); + 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) +} + +pub fn run(file: &Path, json_out: bool, top: usize, key: &wai_quantum::ed25519_dalek::SigningKey, a: &RunArgs) -> ! { + let o = wai_wasmq::open(&read(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 { + let text = String::from_utf8(read(path)).unwrap_or_else(|_| die("the transcript is not UTF-8")); + let unhex = |s: &str| (0..s.len() / 2).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap_or_else(|_| die("a transcript's bytes are hex"))).collect::>(); + let calls: Vec = text + .lines() + .filter(|l| !l.trim().is_empty()) + .map(|l| { + let v: Value = serde_json::from_str(l).unwrap_or_else(|e| die(&format!("the transcript: {e}"))); + wai_wasmq::run::Call { + shot: v["shot"].as_u64().unwrap_or(0), + callout: v["callout"].as_str().unwrap_or("").to_string(), + input: unhex(v["input"].as_str().unwrap_or("")), + output: unhex(v["output"].as_str().unwrap_or("")), + fuel: v["fuel"].as_u64().unwrap_or(0), + } + }) + .collect(); + let problems = wai_wasmq::run::replay(&o, &calls, kind).unwrap_or_else(|e| die(&e.to_string())); + if json_out { + println!("{}", json!({ "calls": calls.len(), "problems": problems })); + } else { + println!("replayed {} calls through the envelope's modules", calls.len()); + if problems.is_empty() { + println!("result each call's output and fuel as the transcript records"); + } + for p in &problems { + println!("differs {p}"); + } + } + std::process::exit(if problems.is_empty() { 0 } else { 1 }) + } + let (body, log) = wai_wasmq::run(&o, kind, key, a.transcript.is_some()).unwrap_or_else(|e| die(&e.to_string())); + if let (Some(path), Some(log)) = (&a.transcript, log) { + let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + let text: String = log.iter().map(|c| json!({ "shot": c.shot, "callout": c.callout, "input": hex(&c.input), "output": hex(&c.output), "fuel": c.fuel }).to_string() + "\n").collect(); + std::fs::write(path, text).unwrap_or_else(|e| die(&format!("{}: {e}", path.display()))); + } + 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 {}, prepared {}", m["name"].as_str().unwrap_or(""), m["blake3"].as_str().unwrap_or(""), m["prepared_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("")); +} + +/// `verify` for a wasmQ job: exit status 0 when it holds. +pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { + let opened = bundle.map(|p| wai_wasmq::open(&read(p)).unwrap_or_else(|r| die(&r.to_string()))); + let verdict = wai_wasmq::verify(v, opened.as_ref(), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); + println!("signature {}", if verdict.signature { "VALID" } else { "INVALID" }); + println!("binds the contract (program, modules, priors, run) and the result: fuel, modules, transcript, fault"); + if verdict.well_formed.is_empty() { + println!("result a result the profile writes: fuel within its budgets, a fault it names"); + } + for p in &verdict.well_formed { + println!("result NOT one the profile writes: {p}"); + } + match verdict.bundle { + Some(true) => println!("bundle the envelope the receipt binds"), + Some(false) => println!("bundle NOT the envelope the receipt binds"), + None => println!("bundle not checked (--bundle FILE runs it again and compares)"), + } + match verdict.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 => {} + } + verdict.ok() +} + +pub fn check(module: &Path, json_out: bool) -> ! { + let bytes = read(module); + match wai_wasmq::profile::check(&bytes) { + Ok(f) => { + let prepared = wai_wasmq::meter::prepare(&bytes).unwrap_or_else(|r| die(&r.to_string())); + let exports: Vec = f.functions.iter().map(|(n, (p, r))| json!({ "name": n, "params": format!("{p:?}"), "results": format!("{r:?}") })).collect(); + let v = json!({ "profile": wai_wasmq::profile::PROFILE, "ok": true, "blake3": wai_wasmq::envelope::blake3_hex(&bytes), "prepared_blake3": wai_wasmq::envelope::blake3_hex(&prepared), "memory_pages": f.memory, "floats": f.floats, "functions": f.defined_functions, "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.defined_functions, f.memory, if f.floats { "floating point (canonical NaNs)" } else { "integers only" }); + println!("prepared blake3 {}", v["prepared_blake3"].as_str().unwrap_or("")); + 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/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs new file mode 100644 index 000000000..4ae7d8106 --- /dev/null +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -0,0 +1,137 @@ +//! 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("no provider runs it"), "{}", text(&o)); + let o = run(&d, &["run", env.to_str().unwrap(), "--shots", "5"]); + assert!(text(&o).contains("states its own shots"), "{}", text(&o)); +} diff --git a/wai/wai-quantum-qpu/bring-your-own-quantum.md b/wai/wai-quantum-qpu/bring-your-own-quantum.md index 92de9f9fd..068013833 100644 --- a/wai/wai-quantum-qpu/bring-your-own-quantum.md +++ b/wai/wai-quantum-qpu/bring-your-own-quantum.md @@ -185,6 +185,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 provider's result is labelled `measured`. Its receipt binds the request, diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index a789a9eb0..352a8aa12 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -254,6 +254,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 @@ -374,6 +434,23 @@ 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 { + let toks = lex::lex(src)?; + let (stmts, version) = parse::parse(src, &toks)?; + let (program, lines, diagnostics, externs, calls) = elab::elaborate_callouts(src, &toks, &stmts, version, inputs)?; + 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 }) +} + /// The integer `input`s a program declares (`input uint n;`, `input int k;`), /// in the order it declares them. A program that declares one is a /// generator: [`parse_with`] reads it for each value, and [`parse`] refuses it, diff --git a/wai/wai-quantum/src/qasm/elab.rs b/wai/wai-quantum/src/qasm/elab.rs index 45b0330f1..b8086544e 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -21,7 +21,7 @@ use super::lex::{Tok, Token, Verb}; use super::parse::Version; use super::value::{self, Angle, Dur, Ty, Val}; 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. @@ -225,6 +225,14 @@ 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, opaque: HashSet, calibrated: HashSet, taken: HashMap, @@ -260,7 +268,34 @@ 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], version: Version, inputs: &Inputs, tan: Option) -> R<(Program, Vec, Vec, Vec)> { - let mut e = Elab { + elaborate_full(src, toks, stmts, version, inputs, tan, false) +} + +/// [`elaborate`] with each call of an `extern` read as a step (`qasm::read_callouts`). +pub(crate) fn elaborate_callouts(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs) -> R<(Program, Vec, Vec, Vec, Vec)> { + let mut e = elab(src, toks, version, inputs, None, true); + 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()?; + 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); + +fn elaborate_full(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs, tan: Option, callouts: bool) -> R<(Program, Vec, Vec, Vec)> { + let mut e = elab(src, toks, version, inputs, tan, callouts); + 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)) +} + +fn elab<'a>(src: &'a str, toks: &'a [Token], version: Version, inputs: &'a Inputs, tan: Option, callouts: bool) -> Elab<'a> { + Elab { src, version, inputs, @@ -282,6 +317,10 @@ pub(crate) fn elaborate_with(src: &str, toks: &[Token], stmts: &[Stmt], version: gates: HashMap::new(), defs: HashMap::new(), externs: HashSet::new(), + callouts, + toks, + extern_decls: Vec::new(), + calls: Vec::new(), opaque: HashSet::new(), calibrated: HashSet::new(), taken: HashMap::new(), @@ -298,13 +337,7 @@ pub(crate) fn elaborate_with(src: &str, toks: &[Token], stmts: &[Stmt], version: ones: BTreeSet::new(), written: BTreeSet::new(), tan, - }; - 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)) + } } fn is_physical(name: &str) -> Option { @@ -754,6 +787,104 @@ 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; + // 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 { @@ -1051,6 +1182,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 } => { @@ -1094,6 +1231,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)?; @@ -1404,6 +1546,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)); } @@ -1495,6 +1640,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)); } @@ -2221,6 +2369,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()))); @@ -3480,6 +3635,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")); } @@ -3603,7 +3761,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, Vec)> { if self.n_qubits == 0 { return Err(Diagnostic::new(diag::NO_QUBITS, None, "the program declares no qubits")); } @@ -3641,6 +3803,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) { @@ -3659,7 +3829,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 522a030e7..1e3405a74 100644 --- a/wai/wai-quantum/src/qasm/lang_tests.rs +++ b/wai/wai-quantum/src/qasm/lang_tests.rs @@ -607,3 +607,54 @@ fn every_refusal_and_warning_carries_a_code_in_the_table() { assert_eq!(r.diagnostics.iter().map(|d| d.code).collect::>(), vec![diag::CARET_POWER, diag::UNKNOWN_INCLUDE, diag::UNKNOWN_VERSION, diag::UNUSED_PRAGMA, diag::UNUSED_PRAGMA]); 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()); +} diff --git a/wai/wai-quantum/src/qasm/parse.rs b/wai/wai-quantum/src/qasm/parse.rs index bd144a8e9..37b916df9 100644 --- a/wai/wai-quantum/src/qasm/parse.rs +++ b/wai/wai-quantum/src/qasm/parse.rs @@ -36,6 +36,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-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index 2c90f3cc5..6c64b7305 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -102,6 +102,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 050af46e5..68423e37f 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -326,6 +326,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 @@ -711,6 +717,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 @@ -962,7 +970,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 @@ -1124,7 +1132,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). @@ -1489,7 +1497,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). @@ -2713,6 +2721,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"), (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 06029ca86..beb5718db 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -344,6 +344,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_QUANTUM_CALIBRATION, S_INTERACTIVE, None, "2026-08-01"), (CAP_QUANTUM_JOB, S_INTERACTIVE, None, "2026-08-01"), @@ -1180,7 +1181,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_TENSOR_NNC | 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..1bbed7fcf --- /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.8.1" +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..e7185b7d1 --- /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.8.1", 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.8.1", 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..cfe32be31 --- /dev/null +++ b/wai/wai-wasmq/README.md @@ -0,0 +1,32 @@ +# 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 bounded, each + rule a refusal code; +- prepares it (`meter`) so that it meters its own fuel, counts its own call depth and + returns only canonical NaNs, so any WebAssembly engine runs it alike; +- 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`), runs a bundle shot by shot with each callout + metered (`run`), and seals the job receipt `wai-quantum verify` checks; +- replays a record of calls through the modules alone (`run::replay`); +- 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 +``` 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..bad49a58f --- /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.8.1" +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/src/abi.rs b/wai/wai-wasmq/src/abi.rs new file mode 100644 index 000000000..9dd9e39a1 --- /dev/null +++ b/wai/wai-wasmq/src/abi.rs @@ -0,0 +1,243 @@ +//! 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 } +} + +/// 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_depth(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..2fa3fd821 --- /dev/null +++ b/wai/wai-wasmq/src/engine.rs @@ -0,0 +1,360 @@ +//! 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_CALL_DEPTH; + +/// 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, + /// More calls in progress 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 => "call_depth_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); + fn set_depth(&mut self, depth: u32); + /// `len` bytes of memory from `at`, if they are inside it. + fn read(&mut self, at: u32, len: u32) -> Option>; + /// Writes `data` at `at`, if it fits inside the memory. + 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 depth +/// check, or the module's own `unreachable`. +pub(crate) fn refine(t: Trap, fuel: i64, depth: u32) -> Trap { + match t { + Trap::Unreachable if fuel < 0 => Trap::Fuel, + Trap::Unreachable if depth > MAX_CALL_DEPTH => 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.depth()) } else { t }) + } + fn fuel(&mut self) -> i64 { + self.i.fuel() + } + fn set_fuel(&mut self, fuel: i64) { + self.i.set_fuel(fuel) + } + fn set_depth(&mut self, depth: u32) { + self.i.set_depth(depth) + } + 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().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}; + +#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] +mod native { + use super::*; + use crate::meter; + use wasmtime::{Config, Engine as WEngine, Module, Store, Val, ValType}; + + /// 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(128 << 20) + .async_stack_size(129 << 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 }) + } + } + + 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, + depth: 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 depth = instance.get_global(&mut store, meter::DEPTH).ok_or("the module is not prepared: no depth global")?; + let memory = instance.exports(&mut store).find_map(|e| e.into_memory()); + Ok(Box::new(NativeInstance { store, instance, fuel, depth, memory })) + } + } + + impl Instance for NativeInstance { + fn call(&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, depth) = (self.fuel(), self.depth.get(&mut self.store).i32().unwrap_or(0) as u32); + Err(refine(t, fuel, depth)) + } + } + } + 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_depth(&mut self, depth: u32) { + let _ = self.depth.set(&mut self.store, Val::I32(depth 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..3bc4f5ac5 --- /dev/null +++ b/wai/wai-wasmq/src/envelope.rs @@ -0,0 +1,346 @@ +//! 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 capability string. +pub const CAPABILITY: &str = "wai.quantum.wasmq"; +/// The payload's magic. +pub const MAGIC: &[u8; 4] = b"WQM1"; +/// The most shots a run takes. +pub const MAX_SHOTS: u64 = 1 << 20; +/// The largest number a budget, a seed or a count may be: every JSON reader holds it exactly. +pub const MAX_EXACT: u64 = (1 << 53) - 1; +/// The most qubits the state vector takes in a run; the stabilizer method takes any. +pub const MAX_DENSE_QUBITS: u32 = 24; + +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_000_000_000_000, 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. +pub fn read(program: &str, inputs: &wai_quantum::qasm::Inputs) -> Result<(wai_quantum::qasm::Hybrid, String), Refusal> { + let h = wai_quantum::qasm::read_callouts(program, inputs).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")); + } + let method = if p.clifford_ops().is_ok() { + "stabilizer" + } else if p.n_qubits <= MAX_DENSE_QUBITS { + "statevector" + } else { + return Err(refuse("W016", format!("{} qubits with gates outside the Clifford group: the runner holds a state vector of at most {MAX_DENSE_QUBITS}", p.n_qubits))); + }; + Ok((h, method.to_string())) +} + +/// Checks a bundle as [`pack`] and [`open`] do: the program, the modules, the callouts +/// against the modules' exports, and the run. The method it fixes. +pub fn check(b: &Bundle) -> Result { + 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"))); + } + } + 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 {}, but the program runs by {method}", r.method))); + } + let mut facts = Vec::new(); + for m in &b.modules { + facts.push(profile::check(&m.bytes).map_err(|e| refuse(e.code, format!("module {}: {}", m.name, e.message)))?); + } + for d in &p.externs { + let c = b.callouts.iter().find(|c| c.name == d.name).ok_or_else(|| refuse("W015", format!("the program declares `extern {}`, and no module is named for it", d.name)))?; + 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 want = abi::core_type(d); + match f.functions.get(&c.export) { + None => return Err(refuse("W015", format!("module {} exports no function {}", b.modules[c.module].name, c.export))), + Some(got) if *got != want => { + return Err(refuse("W015", format!("{} lowers to {:?} -> {:?}; module {} exports {} as {:?} -> {:?}", d.name, want.0, want.1, b.modules[c.module].name, c.export, got.0, got.1))); + } + Some(_) => {} + } + if abi::takes_list(d) && f.functions.get("cabi_realloc").map(|t| t.0.len() == 4 && t.1.len() == 1) != Some(true) { + return Err(refuse("W015", format!("{} passes a list, and module {} exports no cabi_realloc(i32, i32, i32, i32) -> i32", d.name, b.modules[c.module].name))); + } + if abi::uses_memory(d) && f.memory.is_none() { + return Err(refuse("W015", format!("{} passes a list, and module {} has no memory", d.name, b.modules[c.module].name))); + } + } + for c in &b.callouts { + if !p.externs.iter().any(|d| d.name == c.name) { + return Err(refuse("W015", format!("a callout {} that the program does not declare", c.name))); + } + } + for (m, f) in b.modules.iter().zip(&facts) { + if !m.priors.is_empty() && f.functions.get("wasmq-prior").map(|t| t.0.len() == 3 && t.1.is_empty()) != Some(true) { + return Err(refuse("W015", format!("module {} has priors and exports no wasmq-prior(i32, i32, i32)", m.name))); + } + for (name, want) in [("wasmq-seed", (1usize, 0usize)), ("init", (0, 0))] { + if let Some(t) = f.functions.get(name) + && (t.0.len(), t.1.len()) != want + { + return Err(refuse("W012", format!("module {} exports {name} with the wrong type", m.name))); + } + } + } + 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. +pub fn pack(b: &Bundle) -> Result, Refusal> { + let method = check(b)?; + let (h, _) = read(&b.program, &b.inputs)?; + let contract = canonical(&contract(b, &method)); + let mut payload = MAGIC.to_vec(); + u32le(&mut payload, contract.len())?; + payload.extend_from_slice(&contract); + payload.extend_from_slice(b.program.as_bytes()); + for m in &b.modules { + payload.extend_from_slice(&m.bytes); + for (_, prior) in &m.priors { + payload.extend_from_slice(prior); + } + } + let manifest = manifest(b, h.reading.program.n_qubits)?; + 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) +} + +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 { + 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..fc452a926 --- /dev/null +++ b/wai/wai-wasmq/src/interp.rs @@ -0,0 +1,1059 @@ +//! 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_CALL_DEPTH}; +use std::collections::BTreeMap; +use wasmparser::{BlockType, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, 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 depth. + 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, + 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, + 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(), + 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 Parser::new(0).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); + } + _ => {} + } + } + } + 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 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)?; + 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)), + } + } + allocs = fv.into_allocations(); + m.funcs.push(Func { ty, 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), + } + } + + pub fn depth(&self) -> u32 { + match self.m.metering { + Metering::Own => self.depth, + Metering::Module => self.global(meter::DEPTH).unwrap_or(0) as u32, + } + } + + pub fn set_depth(&mut self, d: u32) { + match self.m.metering { + Metering::Own => self.depth = d, + Metering::Module => self.set_global(meter::DEPTH, d 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::DEPTH => v as u32 as u64, + _ => v, + }; + } + } + + pub fn memory(&self) -> &[u8] { + &self.mem + } + + pub fn memory_mut(&mut self) -> &mut [u8] { + &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(); + self.stack.extend_from_slice(args); + 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; + let ins = &f.code[fr.pc]; + 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)); + } + if m.metering == Metering::Own { + self.depth -= 1; + } + } + I::Call(g) => { + let g = *g; + self.enter(&mut frames, g)?; + } + I::CallIndirect(ty) => { + 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), + } + } + } + + fn enter(&mut self, frames: &mut Vec, g: u32) -> Result<(), Trap> { + if self.m.metering == Metering::Own { + self.depth += 1; + if self.depth > MAX_CALL_DEPTH { + return Err(Trap::Stack); + } + } + 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 }); + Ok(()) + } + + 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)?; + 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..36a64af69 --- /dev/null +++ b/wai/wai-wasmq/src/lib.rs @@ -0,0 +1,27 @@ +//! 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 bounded. It is prepared ([`meter`]) so that it meters its own fuel, +//! bounds its own call depth and returns only canonical NaNs, so that every engine runs +//! it with the same results and the same fuel. Two engines run it here ([`engine`]): the +//! pinned native engine and an interpreter of this crate's own ([`interp`]), the only one +//! on wasm32. A run ([`run`]) goes shot by shot, 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..b3550fe05 --- /dev/null +++ b/wai/wai-wasmq/src/meter.rs @@ -0,0 +1,387 @@ +//! The prepared module (extensions/quantum-wasmq.md §4): the module rewritten so that it +//! meters its own fuel, counts its own calls 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`, `memory.fill` and `memory.init`, a charge of the byte count, +//! and before `memory.grow`, a charge of the page count; +//! - around each `call` and `call_indirect`, a count of the calls in progress in the +//! global `__wasmq_depth`, trapping past [`MAX_CALL_DEPTH`]; +//! - 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 depth after a call, and tells a fuel trap (`__wasmq_fuel < 0`) and a depth +//! trap (`__wasmq_depth > MAX_CALL_DEPTH`) from the module's own. + +use crate::profile::{self, MAX_CALL_DEPTH, Refusal}; +use wasmparser::{Operator, Parser, Payload}; + +/// The exported fuel global. +pub const FUEL: &str = "__wasmq_fuel"; +/// The exported call-depth global. +pub const DEPTH: &str = "__wasmq_depth"; + +/// 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) +} + +/// The instruction's variable charge (§4.2), if it has one: `memory.copy`, `memory.fill` +/// and `memory.init` are charged one for each byte, `memory.grow` one for each page. +pub fn variable(op: &Operator<'_>) -> bool { + use Operator::*; + matches!(op, MemoryCopy { .. } | MemoryFill { .. } | MemoryInit { .. } | 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 depth: bool, + pub nan: bool, +} + +impl Parts { + pub const ALL: Parts = Parts { fuel: true, depth: 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, + depth: 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, kept there (in local `t`). +fn charge_top(out: &mut Vec, g: &Globals, t: u32) { + 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.extend_from_slice(&[0xad, 0x7d]); + fuel_check(out, g); +} + +fn depth_in(out: &mut Vec, g: &Globals) { + out.push(0x23); + leb_u(out, g.depth as u64); + out.extend_from_slice(&[0x41, 0x01, 0x6a, 0x24]); + leb_u(out, g.depth as u64); + out.push(0x23); + leb_u(out, g.depth as u64); + out.push(0x41); + leb_s(out, MAX_CALL_DEPTH as i64); + out.extend_from_slice(&[0x4b, 0x04, 0x40, 0x00, 0x0b]); +} + +fn depth_out(out: &mut Vec, g: &Globals) { + out.push(0x23); + leb_u(out, g.depth as u64); + out.extend_from_slice(&[0x41, 0x01, 0x6b, 0x24]); + leb_u(out, g.depth 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; + let mut bodies: Vec> = Vec::new(); + for payload in Parser::new(0).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) => bodies.push(body), + _ => {} + } + } + let g = Globals { fuel: n_globals, depth: n_globals + 1 }; + let n_funcs = func_types.len() as u32; + let helpers = parts.nan && facts.floats; + 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, body) in bodies.iter().enumerate() { + let params = types[func_types[k] as usize]; + 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 decls = &whole[..ops_at]; + 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)); + } + let needs_t = parts.fuel && list.iter().any(|(op, ..)| variable(op)); + let t = params + declared as u32; + 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); + } + let call = matches!(op, Operator::Call { .. } | Operator::CallIndirect { .. }); + if parts.depth && call { + depth_in(&mut f, &g); + } + f.extend_from_slice(&whole[*a..*b]); + if parts.depth && call { + depth_out(&mut f, &g); + } + 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), (DEPTH, g.depth)] { + 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); + } + } + } + } + if let Ok(dump) = std::env::var("WASMQ_DUMP_PREPARED") { + let _ = std::fs::write(dump, &out); + } + // The prepared module is itself a module in the feature set. + let mut v = wasmparser::Validator::new_with_features(profile::features()); + v.validate_all(&out).map_err(|e| Refusal::new("W001", format!("the prepared module does not validate: {}", 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]); + } + + #[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 code bytes all appear, in order, in the prepared code. + assert!(a.len() > m.len()); + 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..6c879e1ef --- /dev/null +++ b/wai/wai-wasmq/src/profile.rs @@ -0,0 +1,473 @@ +//! 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 the first rule it breaks ([`Refusal`]), each rule with a stable code. + +use std::collections::BTreeMap; +use wasmparser::{ + ConstExpr, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, Payload, TypeRef, ValType, Validator, + WasmFeatures, +}; + +/// The profile's name, as a contract and a receipt write it. +pub const PROFILE: &str = "wai.quantum.wasmq/1"; +/// 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. +pub const MAX_MODULE_BYTES: usize = 16 << 20; +/// The most functions a module may define. +pub const MAX_FUNCTIONS: u32 = 1 << 20; +/// The most locals and operands one function's frame may hold at once. +pub const MAX_FRAME_SLOTS: u32 = 4096; +/// The most calls that may be in progress at once inside one host call (§4.3). +pub const MAX_CALL_DEPTH: u32 = 1024; +/// 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"; + +/// 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 (a component, a malformed module, or an instruction no rule below names)"), + ("W002", "the module imports something: a wasmQ module imports nothing"), + ("W003", "the memory: more than one, shared, 64-bit, without a maximum, or a maximum past the profile's"), + ("W004", "a start function: nothing runs when a module is instantiated"), + ("W005", "threads: shared memory or an atomic instruction"), + ("W006", "a vector (SIMD) instruction, relaxed or not"), + ("W007", "a reference, exception, tail-call, garbage-collected or other instruction or type outside Lime1's numbers"), + ("W008", "the table: more than one, a type other than funcref, an instruction that changes it, or larger than the profile allows"), + ("W009", "a function whose locals and operands can pass the profile's frame"), + ("W010", "the module, or its count of functions, is larger than the profile allows"), + ("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 the profile names with the wrong type"), + ("W013", "the envelope: not a WAI1 envelope of this capability, a contract that is not canonical, 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", "a callout whose module does not export it with the signature its `extern` lowers to (§5.2)"), + ("W016", "the program: the reader refuses it, it calls nothing, or its method or size is not one the runner takes"), + ("W017", "the run: shots, seed or a fuel budget outside what the profile and a receipt can hold"), +]; + +/// 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 function index of each exported function. + pub function_index: BTreeMap, + /// The memory's initial and maximum pages, if it has one. + pub memory: Option<(u64, u64)>, + /// The name the memory is exported under, if it is. + pub memory_export: Option, + /// Whether any function uses a floating-point instruction. + pub floats: bool, + /// Functions the module defines. + pub defined_functions: u32, +} + +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 +} + +/// Reads `bytes` as a wasmQ module: its facts, or the first rule it breaks. +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()))); + } + // The shape first, from a permissive reading, so that a module outside the profile is + // refused with the rule it breaks rather than the validator's first complaint. + shape(bytes)?; + let mut v = Validator::new_with_features(features()); + let mut facts = Facts { + functions: BTreeMap::new(), + function_index: BTreeMap::new(), + memory: None, + memory_export: None, + floats: false, + defined_functions: 0, + }; + 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::new(0).parse_all(bytes) { + let payload = payload.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + match v.payload(&payload).map_err(|e| invalid(bytes, e))? { + wasmparser::ValidPayload::Func(func, body) => { + let mut fv = func.into_validator(std::mem::take(&mut allocs)); + let mut locals = 0u64; + let mut lr = body.get_locals_reader().map_err(|e| Refusal::new("W001", e.message().to_string()))?; + let idx = fv.index(); + let params = types[func_types[idx as usize] as usize].params().len() as u64; + for _ in 0..lr.get_count() { + let off = lr.original_position(); + let (n, ty) = lr.read().map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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 {} locals; a frame holds at most {MAX_FRAME_SLOTS} locals and operands", params + locals))); + } + fv.define_locals(off, n, ty).map_err(|e| invalid(bytes, e))?; + } + let mut ops = body.get_operators_reader().map_err(|e| Refusal::new("W001", e.message().to_string()))?; + while !ops.eof() { + let (op, off) = ops.read_with_offset().map_err(|e| Refusal::new("W001", e.message().to_string()))?; + rule(&op, off as usize)?; + facts.floats |= float_op(&op); + fv.op(off, &op).map_err(|e| invalid(bytes, e))?; + 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}"))); + } + } + allocs = fv.into_allocations(); + } + wasmparser::ValidPayload::End(_) => {} + _ => {} + } + match payload { + Payload::TypeSection(r) => { + for rec in r.into_iter_err_on_gc_types() { + let t = rec.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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(|e| Refusal::new("W001", e.message().to_string()))?); + } + facts.defined_functions = func_types.len() as u32; + if facts.defined_functions > MAX_FUNCTIONS { + return Err(Refusal::new("W010", format!("{} functions; the profile takes at most {MAX_FUNCTIONS}", facts.defined_functions))); + } + } + Payload::TableSection(r) => { + for t in r { + let t = t.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + let max = t.ty.maximum.unwrap_or(t.ty.initial); + if max > MAX_TABLE || t.ty.initial > MAX_TABLE { + return Err(Refusal::new("W008", format!("a table of up to {max} entries; the profile takes at most {MAX_TABLE}"))); + } + table_min = Some(t.ty.initial); + } + } + Payload::MemorySection(r) => { + for m in r { + let m = m.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + facts.memory = Some((m.initial, m.maximum.unwrap_or(0))); + } + } + Payload::GlobalSection(r) => { + for g in r { + let g = g.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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(|e| Refusal::new("W001", e.message().to_string()))?; + 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 => { + let t = &types[func_types[e.index as usize] as usize]; + facts.functions.insert(e.name.to_string(), (t.params().to_vec(), t.results().to_vec())); + facts.function_index.insert(e.name.to_string(), e.index); + } + ExternalKind::Memory => facts.memory_export = Some(e.name.to_string()), + _ => {} + } + } + } + Payload::DataSection(r) => { + let size = facts.memory.map(|(min, _)| min << 16).unwrap_or(0); + for d in r { + let d = d.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + if let DataKind::Active { offset_expr, .. } = d.kind { + 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"))); + } + } + } + } + Payload::ElementSection(r) => { + for el in r { + let el = el.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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 + ) +} + +/// The rules Lime1's validator does not hold: the table is never changed and never read +/// as a value (§3.2). +fn rule(op: &Operator<'_>, off: usize) -> Result<(), Refusal> { + use Operator::*; + match op { + TableGet { .. } | TableSet { .. } | TableGrow { .. } | TableSize { .. } | TableFill { .. } | TableCopy { .. } | TableInit { .. } | ElemDrop { .. } => { + Err(Refusal::new("W008", format!("a table instruction at offset {off}: the table is filled once and only `call_indirect` reads it"))) + } + RefNull { .. } | RefFunc { .. } | RefIsNull => Err(Refusal::new("W007", format!("a reference instruction at offset {off}"))), + _ => Ok(()), + } +} + +/// The validator's complaint, with the rule it most likely names. +fn invalid(bytes: &[u8], e: wasmparser::BinaryReaderError) -> Refusal { + let first = classify(bytes); + Refusal::new(first.0, format!("{}{}", first.1, e.message())) +} + +/// What a module outside the profile reaches for, read permissively: the first +/// instruction or construct, with the code it is refused under. +fn classify(bytes: &[u8]) -> (&'static str, String) { + let mut v = Validator::new_with_features(WasmFeatures::all()); + for payload in Parser::new(0).parse_all(bytes) { + let Ok(payload) = payload else { break }; + if let Ok(wasmparser::ValidPayload::Func(_, body)) = v.payload(&payload) { + let Ok(mut ops) = body.get_operators_reader() else { break }; + while !ops.eof() { + let Ok((op, off)) = ops.read_with_offset() else { break }; + let name = format!("{op:?}"); + let head = name.split(|c: char| !c.is_ascii_alphanumeric()).next().unwrap_or(""); + if head.contains("Atomic") || head.starts_with("MemoryAtomic") { + return ("W005", format!("{head} at offset {off}: ")); + } + if (head.starts_with("Return") && head != "Return") || ["Try", "Throw", "Rethrow", "Delegate", "Struct", "Array", "Ref", "BrOn", "Any", "Extern", "I31", "CallRef"].iter().any(|p| head.starts_with(p)) { + return ("W007", format!("{head} at offset {off}: ")); + } + if head.starts_with("V128") || head.starts_with("I8x16") || head.starts_with("I16x8") || head.starts_with("I32x4") || head.starts_with("I64x2") || head.starts_with("F32x4") || head.starts_with("F64x2") { + return ("W006", format!("{head} at offset {off}: ")); + } + } + } + } + ("W001", String::new()) +} + +/// The module's shape: the rules a permissive reading can decide before validation. +fn shape(bytes: &[u8]) -> Result<(), Refusal> { + let mut memories = 0; + for payload in Parser::new(0).parse_all(bytes) { + let payload = payload.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + match payload { + 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::ImportSection(r) => { + if let Some(i) = r.into_imports().next() { + let i = i.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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::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(|e| Refusal::new("W001", e.message().to_string()))?; + 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 custom page size")); + } + 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) => { + let mut n = 0; + for t in r { + let t = t.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + n += 1; + if n > 1 { + return Err(Refusal::new("W008", "more than one table")); + } + if t.ty.element_type != wasmparser::RefType::FUNCREF || t.ty.table64 || t.ty.shared { + return Err(Refusal::new("W008", "a table that is not a 32-bit, unshared table of funcref")); + } + } + } + Payload::TagSection(_) => return Err(Refusal::new("W007", "a tag (exception handling)")), + Payload::GlobalSection(r) => { + for g in r { + let g = g.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + if g.ty.shared { + return Err(Refusal::new("W005", "a shared global")); + } + if !matches!(g.ty.content_type, ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64) { + return Err(Refusal::new(if g.ty.content_type == ValType::V128 { "W006" } else { "W007" }, "a global that is not a number")); + } + } + } + _ => {} + } + } + Ok(()) +} + +#[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 (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 (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"); + } + + #[test] + fn floats_are_noticed() { + 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); + } +} 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..a0c14e9b0 --- /dev/null +++ b/wai/wai-wasmq/src/run.rs @@ -0,0 +1,589 @@ +//! Running a bundle (extensions/quantum-wasmq.md §6): the program's steps shot by shot, +//! 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. + +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::engine::dynamic::{Dense, Engine as QState, Stabilizer}; +use wai_quantum::qasm::{CalloutBit, Instr, Program, Step}; + +/// The result's format. +pub const FORMAT: &str = "wai.quantum.wasmq.result/1"; +/// What a receipt's `energy` says: no meter reads a run here. +pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; + +/// 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) +} + +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) + } + /// Whether a measurement whose outcome is 1 with probability `p` reads 1. + fn reads_one(&mut self, p: f64) -> bool { + ((self.next() >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0 < p + } +} + +struct Budget { + per_call: u64, + job_left: u64, +} + +impl Budget { + /// The fuel a call may use: the call's budget, or what is left of the run's. + fn limit(&self, per: u64) -> (u64, &'static str) { + if per <= self.job_left { (per, "call") } else { (self.job_left, "run") } + } +} + +struct Tally { + used: u64, + init: u64, + calls: u64, + functions: BTreeMap, + transcript: blake3::Hasher, + log: Option>, +} + +/// A run's fault, before it is written into the result. +struct Stopped { + fault: Fault, + shot: u64, + phase: &'static str, + callout: Option, + module: String, + scope: &'static str, + fuel: u64, +} + +/// A lifecycle call of an instance (§5.3), to be metered. +type Lifecycle = Box Result<(), Stop>>; + +/// One metered stretch of guest work: set the fuel, run `f`, read what was used. +fn metered(inst: &mut dyn Instance, b: &mut Budget, per: u64, f: impl FnOnce(&mut dyn Instance) -> Result) -> (Result, u64, &'static str) { + let (limit, scope) = 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, scope) +} + +/// 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> { + let b = &opened.bundle; + let prepared: Vec> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; + let work = || -> Result<(Value, Option>), Error> { + let mut engines: Vec> = Vec::new(); + for (m, p) in b.modules.iter().zip(&prepared) { + engines.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)?), + }); + } + execute(opened, &prepared, &engines, key, log) + }; + // The native engine runs a call's frames on this thread's stack: give it one deep + // enough for the profile's call depth (§4.3). + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::scope(|s| std::thread::Builder::new().stack_size(512 << 20).spawn_scoped(s, work).map_err(|e| Error::Host(e.to_string()))?.join().map_err(|_| Error::Host("the runner's thread panicked".into()))?) + } + #[cfg(target_arch = "wasm32")] + { + work() + } +} + +/// The width of the bits a program's steps read and write. +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, prepared: &[Vec], engines: &'e [Box], key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { + let mut live: Vec>> = engines.iter().map(|_| None).collect(); + 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 mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; + let mut t = Tally { used: 0, init: 0, calls: 0, functions: BTreeMap::new(), transcript: blake3::Hasher::new(), log: log.then(Vec::new) }; + t.transcript.update(b"wai:wasmq-transcript\x01"); + let mut counts: BTreeMap = BTreeMap::new(); + let mut shots_run = 0u64; + let mut stopped: Option = None; + + let gates = if method == "statevector" { p.matrices() } else { Vec::new() }; + let cliffords: Vec> = if method == "stabilizer" { + p.clifford_ops().map_err(|e| Error::Refused(profile::Refusal::new("W016", e)))?.into_iter().map(|(g, _)| g).collect() + } else { + Vec::new() + }; + '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, m) in b.modules.iter().enumerate() { + let mut i = engines[k].instantiate().map_err(Error::Host)?; + let f = &facts[k]; + let seed = if b.run.state == "shot" { shot_seed(b.run.seed, shot) } else { b.run.seed }; + let mut phases: Vec<(&'static str, Lifecycle)> = Vec::new(); + if f.functions.contains_key("wasmq-seed") { + phases.push(("seed", Box::new(move |i: &mut dyn Instance| abi::lifecycle(i, "wasmq-seed", &[seed])))); + } + for (j, (_, bytes)) in m.priors.iter().enumerate() { + let bytes = bytes.clone(); + phases.push(("prior", Box::new(move |i: &mut dyn Instance| abi::prior(i, j as u32, &bytes)))); + } + if f.functions.contains_key("init") { + phases.push(("init", Box::new(|i: &mut dyn Instance| abi::lifecycle(i, "init", &[])))); + } + for (phase, f) in phases { + let (r, used, scope) = metered(i.as_mut(), &mut budget, b.run.per_init, f); + t.used += used; + t.init += used; + match r { + Ok(()) => {} + Err(Stop::Host(h)) => return Err(Error::Host(h)), + Err(Stop::Fault(fault)) => { + let scope = if fault.reason == "fuel_exhausted" { if scope == "call" { "instance" } else { "run" } } else { "" }; + stopped = Some(Stopped { fault, shot, phase, callout: None, module: m.name.clone(), scope, fuel: used }); + break 'shots; + } + } + } + live[k] = Some(i); + } + } + let mut rng = Rng(shot_seed(b.run.seed, shot) ^ 0x5157_4d31); + let mut bits = vec![false; w]; + enum St<'a> { + D(Dense<'a>), + S(Stabilizer<'a>), + } + let mut st = match method.as_str() { + "statevector" => St::D(Dense { sv: wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).map_err(|e| Error::Host(format!("{e:?}")))?, gates: &gates }), + _ => St::S(Stabilizer { t: wai_quantum::quantum_tableau::Tableau::new(p.n_qubits).map_err(|e| Error::Host(e.to_string()))?, ops: &cliffords }), + }; + macro_rules! q { + ($s:ident => $e:expr) => { + match &mut st { + St::D($s) => $e, + St::S($s) => $e, + } + }; + } + let mut pc = 0; + while pc < steps.len() { + match &steps[pc] { + Step::Instr(Instr::Gate(i)) => q!(s => s.gate(*i)).map_err(Error::Host)?, + Step::Instr(Instr::Measure { qubit, bit }) => { + let p1 = q!(s => s.prob_one(*qubit)).map_err(Error::Host)?; + let one = rng.reads_one(p1); + q!(s => s.collapse(*qubit, one)).map_err(Error::Host)?; + bits[*bit as usize] = one; + } + Step::Instr(Instr::Reset(qubit)) => { + let p1 = q!(s => s.prob_one(*qubit)).map_err(Error::Host)?; + let one = rng.reads_one(p1); + q!(s => s.collapse(*qubit, one)).map_err(Error::Host)?; + if one { + q!(s => s.flip(*qubit)).map_err(Error::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.iter().find(|c| c.name == decl.name).ok_or_else(|| Error::Host(format!("no callout for {}", decl.name)))?; + 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, used, scope) = metered(i.as_mut(), &mut budget, per, |i| abi::call(i, decl, &c.export, &ins, has_post)); + t.used += used; + t.calls += 1; + let e = t.functions.entry(decl.name.clone()).or_default(); + e.0 += 1; + e.1 += used; + e.2 = e.2.max(used); + 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]; + } + for part in [&shot.to_le_bytes()[..], &func.to_le_bytes(), &(in_bytes.len() as u32).to_le_bytes(), &in_bytes, &(out_bytes.len() as u32).to_le_bytes(), &out_bytes, &used.to_le_bytes()] { + t.transcript.update(part); + } + if let Some(l) = t.log.as_mut() { + l.push(Call { shot, callout: decl.name.clone(), input: in_bytes, output: out_bytes, fuel: used }); + } + } + Err(Stop::Host(h)) => return Err(Error::Host(h)), + Err(Stop::Fault(fault)) => { + let scope = if fault.reason == "fuel_exhausted" { scope } else { "" }; + stopped = Some(Stopped { fault, shot, phase: "call", callout: Some(decl.name.clone()), module: b.modules[c.module].name.clone(), scope, fuel: used }); + 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) = &stopped { + let mut fault = vec![b"fault".to_vec(), s.shot.to_le_bytes().to_vec(), s.fault.reason.as_bytes().to_vec()]; + fault.push(s.fuel.to_le_bytes().to_vec()); + for part in fault { + t.transcript.update(&part); + } + } + + let modules: Vec = b + .modules + .iter() + .zip(prepared) + .zip(&facts) + .map(|((m, pr), f)| json!({ "name": m.name, "blake3": blake3_hex(&m.bytes), "prepared_blake3": blake3_hex(pr), "memory_pages": f.memory.map(|(a, b)| json!([a, b])).unwrap_or(Value::Null), "floats": f.floats })) + .collect(); + let mut functions = Map::new(); + for (name, (calls, fuel, max)) in &t.functions { + 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": t.calls, + "fuel": { + "used": t.used, + "instances": t.init, + "budget": { "per_call": b.run.per_call, "per_job": b.run.per_job, "per_instance": b.run.per_init }, + "callouts": functions, + }, + "modules": modules, + "transcript_blake3": hex(t.transcript.finalize().as_bytes()), + "accuracy": { + "class": "exact", + "note": "each shot runs alone, its measurements drawn from the run's seed and the shot's number; each callout runs in a metered module with the same results, fuel and fault on every host", + }, + }); + if let Some(s) = stopped { + let mut f = json!({ "reason": s.fault.reason, "shot": s.shot, "phase": s.phase, "module": s.module, "fuel": s.fuel }); + if let Some(c) = s.callout { + f["callout"] = json!(c); + } + if !s.scope.is_empty() { + f["budget"] = json!(s.scope); + } + if !s.fault.detail.is_empty() { + f["detail"] = json!(s.fault.detail); + } + result["fault"] = f; + } + Ok((seal(opened, result, key), t.log)) +} + +/// 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> { + let b = &opened.bundle; + let (h, _) = crate::envelope::read(&b.program, &b.inputs).map_err(Error::Refused)?; + let externs = &h.externs; + let prepared: Vec> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; + let facts: Vec = b.modules.iter().map(|m| profile::check(&m.bytes)).collect::>().map_err(Error::Refused)?; + let work = || -> Result, Error> { + let mut engines: Vec> = Vec::new(); + for (m, pr) in b.modules.iter().zip(&prepared) { + engines.push(match kind { + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + EngineKind::Native => Box::new(engine::Native::new(pr).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(pr, interp::Metering::Module).map_err(Error::Host)?), + }); + } + let mut live: Vec)>> = engines.iter().map(|_| None).collect(); + let mut problems = Vec::new(); + let mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; + for (k, call) in calls.iter().enumerate() { + let Some(func) = externs.iter().position(|d| d.name == call.callout) else { + problems.push(format!("call {k}: the program declares no `extern {}`", call.callout)); + continue; + }; + let decl = &externs[func]; + let c = b.callouts.iter().find(|c| c.name == decl.name).ok_or_else(|| Error::Host(format!("no callout for {}", decl.name)))?; + let fresh = match &live[c.module] { + None => true, + Some((shot, _)) => b.run.state == "shot" && *shot != call.shot, + }; + if fresh { + let mut i = engines[c.module].instantiate().map_err(Error::Host)?; + let seed = if b.run.state == "shot" { shot_seed(b.run.seed, call.shot) } else { b.run.seed }; + let f = &facts[c.module]; + let m = &b.modules[c.module]; + if f.functions.contains_key("wasmq-seed") { + metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::lifecycle(i, "wasmq-seed", &[seed])).0.map_err(|e| Error::Host(format!("{e:?}")))?; + } + for (j, (_, bytes)) in m.priors.iter().enumerate() { + metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::prior(i, j as u32, bytes)).0.map_err(|e| Error::Host(format!("{e:?}")))?; + } + if f.functions.contains_key("init") { + metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::lifecycle(i, "init", &[])).0.map_err(|e| Error::Host(format!("{e:?}")))?; + } + live[c.module] = Some((call.shot, i)); + } + let width: u32 = decl.args.iter().map(|a| a.bits()).sum(); + if call.input.len() != (width as usize).div_ceil(8) { + problems.push(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, |i| abi::call(i, decl, &c.export, &ins, has_post)); + match r { + Ok(outs) => { + let out = abi::pack(&outs); + if out != call.output { + problems.push(format!("call {k} ({} in shot {}): the module returns {}, the record says {}", call.callout, call.shot, hex(&out), hex(&call.output))); + } + if used != call.fuel { + problems.push(format!("call {k} ({} in shot {}): the module uses {used} fuel, the record says {}", call.callout, call.shot, call.fuel)); + } + } + Err(Stop::Fault(f)) => problems.push(format!("call {k} ({} in shot {}): the module stops with {}", call.callout, call.shot, f.reason)), + Err(Stop::Host(h)) => return Err(Error::Host(h)), + } + } + Ok(problems) + }; + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::scope(|s| std::thread::Builder::new().stack_size(512 << 20).spawn_scoped(s, work).map_err(|e| Error::Host(e.to_string()))?.join().map_err(|_| Error::Host("the replay's thread panicked".into()))?) + } + #[cfg(target_arch = "wasm32")] + { + work() + } +} + +/// 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 = if result["status"] == "ok" { "done" } else { "fault" }; + result["receipt"] = receipt; + json!({ "status": status, "method": result["method"], "request": req, "result": result }) +} + +/// What [`verify`] found. +#[derive(Debug, Default)] +pub struct Verdict { + pub signature: bool, + pub well_formed: 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: its signature, that its result is one this profile writes +/// (fuel within budgets, a fault reason it names), and, given the bundle, that it is the +/// bundle the receipt binds and that running it again reproduces the result. +pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result { + let (signature, _) = wai_quantum::engine::job::verify(job)?; + let mut v = Verdict { signature, ..Verdict::default() }; + let r = &job["result"]; + let mut problem = |s: String| v.well_formed.push(s); + if r["format"] != FORMAT { + problem(format!("the result's format is {}, not {FORMAT}", r["format"])); + } + let c = &job["request"]["wasmq"]; + let fuel = &r["fuel"]; + let per_job = c["run"]["fuel"]["per_job"].as_u64().unwrap_or(0); + if fuel["used"].as_u64().is_none_or(|u| u > per_job) { + problem(format!("fuel used {} is past the run's budget {per_job}", fuel["used"])); + } + let per_call = c["run"]["fuel"]["per_call"].as_u64().unwrap_or(0); + for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { + if f["max"].as_u64().is_none_or(|m| m > per_call) { + problem(format!("callout {name} used {} in one call, past the budget {per_call}", f["max"])); + } + } + let mods = c["modules"].as_array().cloned().unwrap_or_default(); + let got = r["modules"].as_array().cloned().unwrap_or_default(); + if mods.len() != got.len() || mods.iter().zip(&got).any(|(a, b)| a["blake3"] != b["blake3"] || a["name"] != b["name"]) { + problem("the result's modules are not the ones the contract pins".into()); + } + match r["status"].as_str() { + Some("ok") => { + if r.get("fault").is_some() { + problem("a result that is ok and names a fault".into()); + } + } + Some("fault") => { + let reasons = ["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", "call_depth_exhausted", "bad_result"]; + if !r["fault"]["reason"].as_str().is_some_and(|x| reasons.contains(&x)) { + problem(format!("a fault the profile does not name: {}", r["fault"]["reason"])); + } + } + _ => problem(format!("status {}", r["status"])), + } + let total: u64 = r["counts"].as_object().into_iter().flatten().filter_map(|(_, n)| n.as_u64()).sum(); + if Some(total) != r["shots_run"].as_u64() { + problem(format!("counts sum to {total}, not the {} shots run", r["shots_run"])); + } + if let Some(o) = bundle { + let same = o.envelope_blake3 == job["request"]["envelope_blake3"] && &o.contract == 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..e340a82b1 --- /dev/null +++ b/wai/wai-wasmq/src/vectors.rs @@ -0,0 +1,85 @@ +//! 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"])); + } + lines.push(format!("{file} refused {}", v["code"].as_str().unwrap_or(""))); + } + 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..f09c08926 --- /dev/null +++ b/wai/wai-wasmq/tests/adversarial.rs @@ -0,0 +1,287 @@ +//! 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_call_depth_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"], "call_depth_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"); +} + +#[test] +fn a_changed_module_program_or_contract_is_refused() { + let env = envelope(); + let bad = tamper(&env, b"wasmq-prior", b"wasmq-PRIOR"); + assert_eq!(open(&bad).unwrap_err().code, "W014", "a module byte"); + let bad = tamper(&env, b"h q;", b"x q;"); + assert_eq!(open(&bad).unwrap_err().code, "W014", "a program byte"); + let bad = tamper(&env, b"\"shots\":4", b"\"shots\":5"); + assert_eq!(open(&bad).unwrap_err().code, "W013", "the contract's run, not the manifest's"); + let bad = tamper(&env, b"\"fallback\":null", b"\"fallback\":0 "); + assert_eq!(open(&bad).unwrap_err().code, "W013", "a manifest byte"); + let mut long = env.clone(); + long.push(0); + assert_eq!(open(&long).unwrap_err().code, "W013", "a byte after the payload"); + assert_eq!(open(&env[..env.len() - 1]).unwrap_err().code, "W013", "a byte short"); +} + +#[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_depth_count_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_depth(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"); + let deep = wai_wasmq::profile::MAX_CALL_DEPTH as u64 + 10; + assert_eq!(go(Parts::ALL, deep).0, Err(Trap::Stack)); + assert!(go(Parts { depth: false, ..Parts::ALL }, deep).0.is_ok(), "this test can fail: no depth count, no bound"); +} diff --git a/wai/wai-wasmq/tests/engines.rs b/wai/wai-wasmq/tests/engines.rs new file mode 100644 index 000000000..b72234710 --- /dev/null +++ b/wai/wai-wasmq/tests/engines.rs @@ -0,0 +1,304 @@ +//! 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), and the fuel each counts for a run is the fuel the native engine's +//! own meter counts on the original module. + +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_depth(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_depth(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_engines_own_count() { + 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(); + 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), ("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(); + assert_eq!((a.0.clone().unwrap(), a.1 as u64), 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_depth(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"); + } + } +} + +#[test] +fn the_call_depth_is_bounded_the_same_everywhere() { + let m = wat::parse_str(PROGRAMS).unwrap(); + let prepared = meter::prepare(&m).unwrap(); + let max = wai_wasmq::profile::MAX_CALL_DEPTH as u64; + for (arg, ok) in [(max - 1, true), (max, false), (max * 4, false)] { + let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "deep", arg); + assert_eq!(own.0.is_ok(), ok, "deep({arg})"); + 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)); + } +} + +#[test] +fn the_deepest_frames_the_profile_allows_fit_every_engine() { + // A function that holds 4000 locals live across a call to itself, at the profile's + // full call depth: every frame as large as §3.2 allows, as deep as §4.3 allows. + let n = 4000; + let mut body = String::new(); + for i in 0..n { + body.push_str(&format!("local.get 0 i64.const {i} i64.add local.set {} ", i + 1)); + } + body.push_str("local.get 0 i64.eqz if (result i64) i64.const 0 else local.get 0 i64.const 1 i64.sub call $deep end "); + for i in 0..n { + body.push_str(&format!("local.get {} i64.add ", i + 1)); + } + let wat = format!("(module (func $deep (param i64) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 call $deep))", "i64 ".repeat(n)); + let m = wat::parse_str(&wat).unwrap(); + assert!(wai_wasmq::profile::check(&m).is_ok()); + let prepared = meter::prepare(&m).unwrap(); + let max = wai_wasmq::profile::MAX_CALL_DEPTH as u64; + let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", max - 1); + assert!(own.0.is_ok(), "{:?}", own.0); + #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] + assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "f", max - 1)); + let _ = prepared; +} 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/vectors.rs b/wai/wai-wasmq/tests/vectors.rs new file mode 100644 index 000000000..af49dfe71 --- /dev/null +++ b/wai/wai-wasmq/tests/vectors.rs @@ -0,0 +1,158 @@ +//! The conformance vectors (`wai/quantum-conformance/wasmq/`) hold on every engine of this +//! build. `WASMQ_BLESS=1` writes them from the reference bundles and the adversarial +//! modules here; without it the test only reads them. + +use std::path::{Path, PathBuf}; +use wai_quantum::quantum_qec::CssCode; +use wai_wasmq::envelope::{Bundle, Callout, Module, Run, pack}; +use wai_wasmq::reference; +use wai_wasmq::run::EngineKind; +use serde_json::{Value, json}; + +fn dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq") +} + +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 one(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 = wat::parse_str(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))"#).unwrap(); + let deep = wat::parse_str(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))"#).unwrap(); + let nan = wat::parse_str(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))"#).unwrap(); + let grow = wat::parse_str(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))"#).unwrap(); + 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))), + ("fault-fuel.wai", one(spin, tight)), + ("fault-depth.wai", one(deep, Run::new(2, 1))), + ("nan-canonical.wai", one(nan, Run::new(8, 2))), + ("grow-past-maximum.wai", one(grow, Run::new(16, 5))), + ] +} + +/// Modules a runner refuses, each as WebAssembly text and its code. +const REFUSE_WAT: &[(&str, &str, &str)] = &[ + ("import-clock", "W002", r#"(module (import "wasi:clocks/monotonic-clock" "now" (func (result i64))) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("import-random", "W002", r#"(module (import "wasi_snapshot_preview1" "random_get" (func (param i32 i32) (result i32))) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("memory-unbounded", "W003", r#"(module (memory 1) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("memory-too-large", "W003", r#"(module (memory 1 65536) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("start-function", "W004", r#"(module (func $s) (start $s) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("shared-memory", "W005", r#"(module (memory 1 1 shared) (func (export "f") (param i64) (result i64) local.get 0))"#), + ("atomic", "W005", r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) i32.const 0 local.get 0 i64.atomic.rmw.add))"#), + ("simd", "W006", r#"(module (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat i64x2.extract_lane 0))"#), + ("relaxed-simd", "W006", r#"(module (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))"#), + ("tail-call", "W007", r#"(module (func $g (param i64) (result i64) local.get 0) (func (export "f") (param i64) (result i64) local.get 0 return_call $g))"#), + ("table-grow", "W007", r#"(module (table 1 funcref) (func (export "f") (param i64) (result i64) ref.null func i32.const 1 table.grow 0 i64.extend_i32_u))"#), + ("table-size", "W008", r#"(module (table 1 funcref) (func (export "f") (param i64) (result i64) table.size 0 i64.extend_i32_u))"#), + ("data-past-memory", "W011", r#"(module (memory 1 1) (data (i32.const 65530) "0123456789") (func (export "f") (param i64) (result i64) local.get 0))"#), + ("reserved-export", "W012", r#"(module (global (export "__wasmq_fuel") (mut i64) (i64.const 0)) (func (export "f") (param i64) (result i64) local.get 0))"#), +]; + +fn bless() { + let d = dir(); + std::fs::create_dir_all(d.join("envelopes")).unwrap(); + std::fs::create_dir_all(d.join("refuse")).unwrap(); + let key = wai_wasmq::vectors::key(); + let mut index_runs = Vec::new(); + for (name, b) in runs() { + let env = pack(&b).unwrap(); + let path = format!("envelopes/{name}"); + std::fs::write(d.join(&path), &env).unwrap(); + let o = wai_wasmq::open(&env).unwrap(); + let (body, _) = wai_wasmq::run(&o, EngineKind::Interpreter, &key, false).unwrap(); + index_runs.push(json!({ "file": path, "envelope_blake3": wai_wasmq::envelope::blake3_hex(&env), "expect": wai_wasmq::vectors::pins(&body) })); + if name == "surface3-decode64.wai" { + std::fs::write(d.join("envelopes/surface3-decode64.job.json"), serde_json::to_string_pretty(&body).unwrap() + "\n").unwrap(); + std::fs::write(d.join("envelopes/surface3-decode64.qasm"), &b.program).unwrap(); + } + } + let mut index_refuse = Vec::new(); + for (name, code, wat) in REFUSE_WAT { + let bytes = wat::parse_str(wat).unwrap(); + std::fs::write(d.join(format!("refuse/{name}.wat")), format!("{wat}\n")).unwrap(); + std::fs::write(d.join(format!("refuse/{name}.wasm")), &bytes).unwrap(); + index_refuse.push(json!({ "file": format!("refuse/{name}.wasm"), "code": code })); + } + // Envelopes changed after packing: each byte a contract pins, and the bytes around them. + let base = std::fs::read(d.join("envelopes/surface3-decode64.wai")).unwrap(); + let flip = |find: &[u8], with: &[u8]| { + let at = base.windows(find.len()).position(|w| w == find).unwrap(); + let mut e = base.clone(); + 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.clone(); + 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.clone(); + let last = wrong_module.len() - 1; + wrong_module[(module_at + 200).min(last)] ^= 0x40; + let mut trailing = base.clone(); + trailing.push(0); + for (name, code, bytes) in [ + ("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", flip(b"\"shots\":32", b"\"shots\":33")), + ("trailing-byte", "W013", trailing), + ("truncated", "W013", base[..base.len() - 1].to_vec()), + ("other-capability", "W013", flip(b"wai.quantum.wasmq", b"wai.quantum.wasmx")), + ] { + std::fs::write(d.join(format!("refuse/{name}.wai")), &bytes).unwrap(); + index_refuse.push(json!({ "file": format!("refuse/{name}.wai"), "code": code })); + } + let doc = 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, + }); + std::fs::write(d.join("vectors.json"), serde_json::to_string_pretty(&doc).unwrap() + "\n").unwrap(); +} + +fn kinds() -> Vec { + wai_wasmq::engine::available().iter().map(|s| EngineKind::parse(s).unwrap()).collect() +} + +#[test] +fn the_vectors_hold_on_every_engine_of_this_build() { + if std::env::var("WASMQ_BLESS").is_ok() { + bless(); + } + let (lines, problems) = wai_wasmq::vectors::verify(&dir(), &kinds()).unwrap(); + assert!(problems.is_empty(), "{problems:#?}"); + let index: Value = serde_json::from_slice(&std::fs::read(dir().join("vectors.json")).unwrap()).unwrap(); + assert_eq!(lines.len(), index["runs"].as_array().unwrap().len() + index["refuse"].as_array().unwrap().len()); + assert!(lines.len() >= 30, "{} vectors", lines.len()); +} + +#[test] +fn each_refusal_vector_is_refused_only_for_its_defect() { + if std::env::var("WASMQ_BLESS").is_ok() { + return; // the other test is writing the files + } + // The positive control of every module refusal: the same module without the defect + // passes (the identity function), so each vector's code is its defect's. + assert!(wai_wasmq::profile::check(&wat::parse_str(r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) local.get 0))"#).unwrap()).is_ok()); + // And the envelope refusals' base opens. + assert!(wai_wasmq::open(&std::fs::read(dir().join("envelopes/surface3-decode64.wai")).unwrap()).is_ok()); + let _ = runs; +} 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); +} -- 2.54.0 (Apple Git-157) From 4d3862abdd019a56cf558ecf5f54fdc8db312fbd Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 15:00:27 -0500 Subject: [PATCH 02/17] wasmq: the deepest frames the profile allows are shown to be real, and the verbs are in the command line's README - The call-depth test now holds 4000 locals live across each call (read from memory the callee writes, so no compiler can make them again after the call), at the full depth, on both engines, and shows that the native engine's own limit, set to 1 MiB, stops the same call: the stack limit the profile sets is what lets it through. - The command line's README and header describe pack-wasmq, check-wasmq, run and verify of an envelope. Co-Authored-By: Claude Fable 5.1 --- wai/extensions/quantum-wasmq.md | 4 +++- wai/wai-quantum-cli/README.md | 23 +++++++++++++++++++++ wai/wai-quantum-cli/src/main.rs | 3 +++ wai/wai-wasmq/src/engine.rs | 11 ++++++++++ wai/wai-wasmq/tests/engines.rs | 36 ++++++++++++++++++++++++--------- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 62a4c4d09..0bc024c22 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -229,7 +229,9 @@ A host call starts at depth 0. Each `call` and `call_indirect` raises the depth before it calls, and lowers it by one when the callee returns. A call that would raise it past **1024** stops with `call_depth_exhausted`. With the frame rule of §3.2, a call within the depth holds at most 1024 frames of at most 4096 slots on any engine, which the -reference engines hold; so no conforming module meets an engine's own stack limit. +reference engines hold; so no conforming module meets an engine's own stack limit. A test +runs a function of 4000 live locals at the full depth on both engines, and shows that the +native engine's limit, set to 1 MiB, would have stopped it (`wai-wasmq/tests/engines.rs`). ### 4.4 The prepared module diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 25e471037..49232c81e 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -287,6 +287,29 @@ 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)): + +```sh +wai-quantum check-wasmq decoder.wasm # the module against the profile: nothing imported, memory bounded +wai-quantum pack-wasmq round.qasm --callout decode64=decoder.wasm \ + --prior decoder.wasm=checks.bin --shots 1000 --fuel-per-call 200000 -o round.wai +wai-quantum run round.wai --json > job.json # each call metered; the same bytes on every host +wai-quantum verify job.json --bundle round.wai # the receipt, and the envelope run again and compared +``` + +A call that runs past its fuel, recurses past the call depth or traps ends the run in a +receipt that names the fault and still verifies. `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 and +checks each call. 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/src/main.rs b/wai/wai-quantum-cli/src/main.rs index bdbeafc40..3f29eb10c 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 completions zsh # shell completion //! ``` //! diff --git a/wai/wai-wasmq/src/engine.rs b/wai/wai-wasmq/src/engine.rs index 2fa3fd821..6000ee40e 100644 --- a/wai/wai-wasmq/src/engine.rs +++ b/wai/wai-wasmq/src/engine.rs @@ -204,6 +204,17 @@ mod native { 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 the + /// profile's: only the test that shows the profile's deepest frames are real, and + /// fit the limit `new` sets, uses 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 { diff --git a/wai/wai-wasmq/tests/engines.rs b/wai/wai-wasmq/tests/engines.rs index b72234710..7acca923b 100644 --- a/wai/wai-wasmq/tests/engines.rs +++ b/wai/wai-wasmq/tests/engines.rs @@ -280,25 +280,43 @@ fn the_call_depth_is_bounded_the_same_everywhere() { #[test] fn the_deepest_frames_the_profile_allows_fit_every_engine() { - // A function that holds 4000 locals live across a call to itself, at the profile's - // full call depth: every frame as large as §3.2 allows, as deep as §4.3 allows. + // A function that holds 4000 locals live across a call to itself, at the profile's full + // call depth: every frame as large as §3.2 allows, as deep as §4.3 allows. Each local + // is read from memory the callee goes on to write, so a compiler can neither read it + // again after the call nor compute it there: it has to keep all 4000 in the frame. let n = 4000; - let mut body = String::new(); + let mut body = String::from("local.get 0 i64.const 4000 i64.rem_u i32.wrap_i64 i32.const 8 i32.mul local.get 0 i64.store "); for i in 0..n { - body.push_str(&format!("local.get 0 i64.const {i} i64.add local.set {} ", i + 1)); + body.push_str(&format!("i32.const {} i64.load local.set {} ", 8 * i, i + 1)); } body.push_str("local.get 0 i64.eqz if (result i64) i64.const 0 else local.get 0 i64.const 1 i64.sub call $deep end "); - for i in 0..n { - body.push_str(&format!("local.get {} i64.add ", i + 1)); + for i in 1..=n { + body.push_str(&format!("local.get {i} i64.add ")); } - let wat = format!("(module (func $deep (param i64) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 call $deep))", "i64 ".repeat(n)); + let wat = format!("(module (memory 1 1) (func $deep (param i64) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 call $deep))", "i64 ".repeat(n)); let m = wat::parse_str(&wat).unwrap(); assert!(wai_wasmq::profile::check(&m).is_ok()); let prepared = meter::prepare(&m).unwrap(); let max = wai_wasmq::profile::MAX_CALL_DEPTH as u64; let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", max - 1); assert!(own.0.is_ok(), "{:?}", own.0); + assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", max - 1)); #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] - assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "f", max - 1)); - let _ = prepared; + { + // On a thread with the stack a run has (`run::run`). + let p = prepared.clone(); + let (full, small) = std::thread::Builder::new() + .stack_size(512 << 20) + .spawn(move || { + let full = fuel_of(&wai_wasmq::engine::Native::new(&p).unwrap(), "f", max - 1); + let small = fuel_of(&wai_wasmq::engine::Native::with_stack(&p, 1 << 20).unwrap(), "f", max - 1); + (full, small) + }) + .unwrap() + .join() + .unwrap(); + assert_eq!(own, full, "the native engine, under the stack limit the profile sets"); + // This test can fail: the frames are real, and an engine limit of 1 MiB stops the call. + assert!(matches!(small.0, Err(Trap::Host(_))), "{:?}", small.0); + } } -- 2.54.0 (Apple Git-157) From 5170e0f1ef226c3f9704752df176dae3d8f7bf86 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 16:50:28 -0500 Subject: [PATCH 03/17] wasmq: one envelope gives one receipt on every engine, verify names the signer, and the profile refuses what it cannot run the same A review of the profile found ways for one envelope to seal two receipts, for an edit outside the signature to verify, and for malformed input to end in a panic. Each has a test that failed on the reviewed code and passes here (tests/review.rs, tests/fuzz.rs). One envelope, one receipt - The host reaches a module's memory only through the export named `memory`, on every engine; a bundle whose callouts or priors need a memory the host cannot reach, or lack `cabi_realloc`, is refused on reading (W015). Exports the host calls have exact types (W012), and a contract names exactly one callout for each extern, in the order declared. - The call-depth count is replaced by a weighted stack: each call adds the calling function's weight (576 + parameters + results + locals + the bytes of its code entry), the limit is 2^23, and the fault is `stack_exhausted`. An indirect call adds the weight before the table is read, so the reason is the same on every engine. The native engine runs each call on a thread with a stack of its own, and a test holds it to eight bytes for each unit of weight. - The interpreter reads an argument as a value of its parameter's type, and refuses a body that does not end. - Profile v1 is the Clifford gates and the `stabilizer` method, to 32,768 qubits; a program outside that is refused on reading (W016). The state-vector method is left out of the profile, and the text says so. A measurement or reset draws from the shot's integer stream only when its outcome is random, and the text states it. What a receipt says, and what verify checks - verify names the signer (did:key) and the outcome, holds the job body to the members the text defines, and refuses a body whose status or method differs from the signed result. - A fault's free text and the accuracy note are out of the signed result; a prepared module's digest is out too, since the text does not determine its bytes. Malformed input - profile::check scans a module without a validator, then validates it under the pinned feature set and finishes every body; a module, envelope or program changed at random is refused or runs the same on every engine, and none ends in a panic. - Limits are explicit and each is tested at its largest value and one more (W010); what pack accepts, every engine can instantiate. - A program input, seed or budget a double cannot hold is refused (W017), not changed. Command line - Builds again without the `providers` feature. `pack-wasmq` takes the global `--input` and orders callouts itself; `verify` prints the signer and the outcome. Vectors and CI - 55 vectors: every code W001 to W017 has a refusal vector, each refusal has a repaired twin that is accepted, and the committed files must equal the ones generated. - The wasmQ workflow runs on a change to the simulators, and checks that every test file ran; the command line's workflow runs on a change to the wasmQ crate or its vectors. - The text no longer describes other systems; the comparative-prose check gains the pattern that sentence passed. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/wai-quantum-cli.yml | 4 + .github/workflows/wai-quantum-wasmq.yml | 51 +- scripts/check-comparative-prose.sh | 1 + scripts/test-public-text-checks.sh | 4 + wai/SPEC.md | 2 +- ...26-11-05-wasmq-hybrid-quantum-classical.md | 51 +- wai/extensions/quantum-wasmq.md | 405 +++++++----- wai/extensions/quantum-wasmq.wit | 5 +- wai/quantum-conformance/wasmq/README.md | 6 +- .../wasmq/envelopes/fault-bad-result.wai | Bin 0 -> 845 bytes .../{fault-depth.wai => fault-stack.wai} | Bin .../wasmq/envelopes/fault-trap.wai | Bin 0 -> 849 bytes .../wasmq/envelopes/list-echo.wai | Bin 0 -> 910 bytes .../wasmq/envelopes/reset-draws.wai | Bin 0 -> 877 bytes .../envelopes/surface3-decode64.job.json | 15 +- .../wasmq/refuse/atomic.wasm | Bin 47 -> 53 bytes .../wasmq/refuse/atomic.wat | 2 +- .../wasmq/refuse/body-without-end.wasm | Bin 0 -> 26 bytes .../wasmq/refuse/byte-after-the-frames.wai | Bin 0 -> 41455 bytes .../wasmq/refuse/callout-of-another-type.wai | Bin 0 -> 841 bytes .../wasmq/refuse/callouts-out-of-order.wai | Bin 0 -> 928 bytes .../wasmq/refuse/contract-extra-member.wai | Bin 0 -> 41464 bytes .../wasmq/refuse/contract-not-canonical.wai | Bin 0 -> 41455 bytes .../wasmq/refuse/contract-run-changed.wai | Bin 41454 -> 41454 bytes .../wasmq/refuse/frame-too-large.wasm | Bin 0 -> 27 bytes .../wasmq/refuse/frame-too-large.wat | 1 + .../wasmq/refuse/function-of-no-type.wasm | Bin 0 -> 24 bytes .../gate-outside-the-clifford-gates.wai | Bin 0 -> 849 bytes .../wasmq/refuse/instances-of-no-kind.wai | Bin 0 -> 844 bytes .../wasmq/refuse/manifest-changed.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/memory-not-exported.wai | Bin 0 -> 901 bytes .../wasmq/refuse/memory-too-large.wasm | Bin 43 -> 42 bytes .../wasmq/refuse/memory-too-large.wat | 2 +- .../wasmq/refuse/no-shots.wai | Bin 0 -> 841 bytes .../wasmq/refuse/payload-of-another-kind.wai | Bin 0 -> 41454 bytes .../wasmq/refuse/program-calls-nothing.wai | Bin 0 -> 846 bytes .../wasmq/refuse/ref-null.wasm | Bin 0 -> 49 bytes .../wasmq/refuse/ref-null.wat | 1 + .../wasmq/refuse/reference-type.wasm | Bin 0 -> 43 bytes .../wasmq/refuse/reference-type.wat | 1 + .../wasmq/refuse/relaxed-simd.wasm | Bin 51 -> 63 bytes .../wasmq/refuse/relaxed-simd.wat | 2 +- .../refuse/seed-a-double-cannot-hold.wai | Bin 0 -> 871 bytes .../wasmq/refuse/seed-of-another-type.wasm | Bin 0 -> 56 bytes .../wasmq/refuse/seed-of-another-type.wat | 1 + .../wasmq/refuse/simd.wasm | Bin 40 -> 52 bytes wai/quantum-conformance/wasmq/refuse/simd.wat | 2 +- .../wasmq/refuse/table-grow.wasm | Bin 47 -> 50 bytes .../wasmq/refuse/table-grow.wat | 2 +- .../wasmq/refuse/table-size.wasm | Bin 43 -> 49 bytes .../wasmq/refuse/table-size.wat | 2 +- .../wasmq/refuse/tail-call.wasm | Bin 56 -> 62 bytes .../wasmq/refuse/tail-call.wat | 2 +- .../wasmq/refuse/too-many-exports.wasm | Bin 0 -> 7119 bytes .../wasmq/refuse/too-many-exports.wat | 1 + .../refuse/two-callouts-for-one-extern.wai | Bin 0 -> 880 bytes wai/quantum-conformance/wasmq/vectors.json | 256 ++++++-- wai/wai-quantum-cli/README.md | 6 +- wai/wai-quantum-cli/docs/llms-full.txt | 7 +- wai/wai-quantum-cli/src/main.rs | 4 +- wai/wai-quantum-cli/src/wasmq.rs | 58 +- wai/wai-quantum-cli/tests/wasmq.rs | 66 +- wai/wai-rs/src/codecs/mod.rs | 2 +- wai/wai-wasmq/README.md | 13 +- wai/wai-wasmq/src/abi.rs | 2 +- wai/wai-wasmq/src/engine.rs | 87 ++- wai/wai-wasmq/src/envelope.rs | 150 +++-- wai/wai-wasmq/src/interp.rs | 78 ++- wai/wai-wasmq/src/lib.rs | 16 +- wai/wai-wasmq/src/meter.rs | 196 ++++-- wai/wai-wasmq/src/profile.rs | 555 ++++++++++------- wai/wai-wasmq/src/run.rs | 512 ++++++++-------- wai/wai-wasmq/tests/adversarial.rs | 75 ++- wai/wai-wasmq/tests/engines.rs | 168 +++-- wai/wai-wasmq/tests/fuzz.rs | 245 ++++++++ wai/wai-wasmq/tests/review.rs | 580 ++++++++++++++++++ wai/wai-wasmq/tests/vectors.rs | 348 ++++++++--- 77 files changed, 2891 insertions(+), 1096 deletions(-) create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-bad-result.wai rename wai/quantum-conformance/wasmq/envelopes/{fault-depth.wai => fault-stack.wai} (100%) create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-trap.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/list-echo.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/reset-draws.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/body-without-end.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/frame-too-large.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/frame-too-large.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/function-of-no-type.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/gate-outside-the-clifford-gates.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/manifest-changed.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/no-shots.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/ref-null.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/ref-null.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/reference-type.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/reference-type.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/seed-a-double-cannot-hold.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/seed-of-another-type.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/too-many-exports.wasm create mode 100644 wai/quantum-conformance/wasmq/refuse/too-many-exports.wat create mode 100644 wai/quantum-conformance/wasmq/refuse/two-callouts-for-one-extern.wai create mode 100644 wai/wai-wasmq/tests/fuzz.rs create mode 100644 wai/wai-wasmq/tests/review.rs diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml index 8c2bc37e3..1a46d7271 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -21,7 +21,9 @@ on: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/wai-wasmq/**' - 'wai/quantum-conformance/eval/**' + - 'wai/quantum-conformance/wasmq/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' pull_request: @@ -29,7 +31,9 @@ on: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/wai-wasmq/**' - 'wai/quantum-conformance/eval/**' + - 'wai/quantum-conformance/wasmq/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' diff --git a/.github/workflows/wai-quantum-wasmq.yml b/.github/workflows/wai-quantum-wasmq.yml index 54aa1ead1..8e3a22ff9 100644 --- a/.github/workflows/wai-quantum-wasmq.yml +++ b/.github/workflows/wai-quantum-wasmq.yml @@ -18,12 +18,7 @@ on: - 'wai/wai-wasmq/**' - 'wai/quantum-conformance/wasmq/**' - 'wai/extensions/quantum-wasmq.*' - - 'wai/wai-quantum/src/qasm.rs' - - 'wai/wai-quantum/src/qasm/**' - - 'wai/wai-quantum/src/engine/dynamic.rs' - - 'wai/wai-quantum/src/quantum_qec.rs' - - 'wai/wai-quantum/src/quantum_atom.rs' - - 'wai/wai-quantum/src/quantum_ops.rs' + - 'wai/wai-quantum/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-wasmq.yml' pull_request: @@ -31,12 +26,7 @@ on: - 'wai/wai-wasmq/**' - 'wai/quantum-conformance/wasmq/**' - 'wai/extensions/quantum-wasmq.*' - - 'wai/wai-quantum/src/qasm.rs' - - 'wai/wai-quantum/src/qasm/**' - - 'wai/wai-quantum/src/engine/dynamic.rs' - - 'wai/wai-quantum/src/quantum_qec.rs' - - 'wai/wai-quantum/src/quantum_atom.rs' - - 'wai/wai-quantum/src/quantum_ops.rs' + - 'wai/wai-quantum/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-wasmq.yml' @@ -86,25 +76,15 @@ jobs: run: | set -euo pipefail cargo test --locked --release --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/wasmq.log" - n=$(grep -c '^test result: ok\.' "$RUNNER_TEMP/wasmq.log" || true) - [ "$n" -ge 7 ] || { echo "::error::$n test targets passed; the crate has 7 (the library, the binary, engines, reference, adversarial, vectors, wit)"; exit 1; } - if grep -q 'FAILED\|test result: FAILED' "$RUNNER_TEMP/wasmq.log"; then echo "::error::a test failed"; exit 1; fi - for t in every_instruction_gives_the_same_value_or_trap_on_both_engines \ - the_fuel_is_the_same_on_every_engine_and_is_the_native_engines_own_count \ - a_run_that_runs_out_stops_at_the_same_charge_everywhere \ - the_call_depth_is_bounded_the_same_everywhere \ - the_deepest_frames_the_profile_allows_fit_every_engine \ - the_decoder_module_decodes_as_the_engine_does_on_every_engine \ - the_planner_module_plans_as_the_engine_does_on_every_engine \ - a_memory_round_decodes_in_the_middle_of_the_circuit_and_every_shot_ends_in_the_code \ - atoms_are_rearranged_in_the_middle_of_the_circuit_and_fill_the_target \ - without_canonicalisation_a_nan_path_reads_what_the_host_chose \ - a_callout_past_its_fuel_ends_in_a_signed_fault_receipt \ - a_prior_whose_hash_is_not_the_pinned_one_is_refused \ - the_vectors_hold_on_every_engine_of_this_build \ - the_reference_modules_export_what_their_worlds_lower_to; do - grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/wasmq.log" || { echo "::error::$t did not run and pass"; exit 1; } + 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 @@ -189,10 +169,13 @@ jobs: 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 --test engines --test reference --test adversarial --test vectors --test wit 2>&1 | tee "$RUNNER_TEMP/wasip2.log" - if grep -q 'FAILED\|test result: FAILED' "$RUNNER_TEMP/wasip2.log"; then echo "::error::a test failed under wasip2"; exit 1; fi - n=$(grep -c '^test result: ok\.' "$RUNNER_TEMP/wasip2.log" || true) - [ "$n" -ge 6 ] || { echo "::error::$n test targets passed under wasip2; 6 run there"; exit 1; } + --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: | 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/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/SPEC.md b/wai/SPEC.md index 71fdc74d2..469046856 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -938,7 +938,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, 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): every conforming runner gives the same result bytes, the same fuel for every callout and the same fault, on every host and engine; each module held to a deterministic profile (Lime1, nothing imported, memory bounded, NaNs canonical, fuel and call depth counted inside the module) and the run sealed in the job receipt. The quantum part is simulated; on hardware the classical part replays from a record of its calls (§7 there) | +| `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 index 84c96cf30..88d5d8b42 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -5,36 +5,45 @@ 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 has no start function, threads, vectors, references or growing - table; each rule has a refusal code (`W001`–`W017`). WebAssembly 3.0's deterministic profile - leaves `memory.grow`, `table.grow` and the call depth to the host; the profile fixes all - three. -- **The same bytes and fuel everywhere.** A module is prepared: rewritten, keeping every - instruction, so that it charges its own fuel at the start of each run of instructions, - counts its own calls to a depth of 1024, and returns only canonical NaNs. The fuel is the - pinned wasmtime's own default count, which a test holds equal on every kind of control - flow, call and memory instruction. Two engines run every module, the pinned wasmtime and - an interpreter of the crate's own that also runs on wasm32, and every instruction, fuel - charge, trap and fault agrees between them. + 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. +- **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. The stack is + bounded by weight: each function's frame weighs a base and its parameters, results, locals + and 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. + 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' and prepared - modules' digests, a digest of every call's inputs and outputs, and the fault that stopped - it (fuel, call depth, a trap or a result past its width), as a signed fault receipt. + 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`), and `verify JOB --bundle ENVELOPE.wai`, which runs - the envelope again and compares byte for byte. 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. + (`--engine`, `--transcript`, `--replay`), and `verify JOB --bundle ENVELOPE.wai`, which names + the signer and how the run ended, refuses a body edited outside the signature, and runs the + envelope again to compare byte for byte. 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. - **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, with their codes. + 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/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 0bc024c22..9fa4f1ed1 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -13,29 +13,31 @@ 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, plans the moves that fill an atom array, -updates a parameter from what it measured. 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: +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 is bounded, and which uses nothing whose result a host - may choose (§3). -- **The same bytes and the same fuel everywhere.** A module is *prepared* (§4): rewritten - so that it meters its own fuel, counts its own calls and returns only canonical NaNs. - Any WebAssembly engine then runs it with the same results, the same fuel and the same - fault. Two engines run it in the reference, the pinned native engine and an interpreter - that also runs on wasm32, and every vector holds on both, on every host. + 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). -A run here simulates the quantum program and runs the classical code for real. §7 says -what holds when the quantum part runs on hardware. +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 @@ -53,6 +55,9 @@ what holds when the quantum part runs on hardware. 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 @@ -90,15 +95,15 @@ frames = program | for each module, in order: the module, then each of its prio | `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` | for each `extern` the program declares: `{ "extern", "module", "export" }`, the module by its index | +| `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 the method the program runs by: -`stabilizer` when every gate is a Clifford gate, `statevector` otherwise, for at most 24 -qubits. `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 each budget -are at most 2^53 − 1, so that every JSON reader holds them exactly. +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`, each +budget and the size of each input are at most 2^53 − 1, so that every JSON reader holds +them exactly. Every module is named by at least one callout. ### 2.3 Reading an envelope @@ -112,16 +117,24 @@ A reader MUST refuse an envelope, with the code of §3.3, when: 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), the program is refused by the reader or calls - nothing (`W016`), a callout does not match its module's export (§5.2, `W015`), or the - run is outside §2.2's ranges (`W017`). +- a module breaks the profile (§3: its code); +- the program is refused by the reader, calls nothing, has a gate outside §5.1's, or + declares more than 32 768 qubits (`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 (`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 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. +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 @@ -137,52 +150,60 @@ component host can wrap it, and a host of this profile calls it directly. 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. And: +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, 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 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). | -| No shared memory, no atomic instruction. | Lime1 has no threads; a shared memory is refused by name. | -| No vector instruction, relaxed or not. | A relaxed vector instruction's result is the engine's choice; the profile takes none. | -| No reference, exception, tail-call or garbage-collection instruction or type. | Lime1's values are numbers. | +| 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 locals and operands at once. | It bounds what one frame takes, so the call depth (§4.3) bounds the stack on every engine. | -| At most 16 MiB of bytes and 2^20 functions. | | +| In every function, at most 4096 parameters, locals and operands at once. | It bounds what one frame holds in an interpreter. | +| At most 4 MiB of bytes; 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^17 bytes. | They bound what a reader, an engine and a frame need (§4.3), with room for what preparing a module adds (§4.4). | | Every active data and element segment fits where it is placed. | Instantiation then cannot fail. | -| No export begins `__wasmq`. | The prepared module's names (§4.4). | +| 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 two rules (§4.5) and closes the others: the -table never grows, the memory grows only within the maximum the module declares (§4.7), -and the call depth is counted (§4.3). +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 +prepares a module at all of them at once (`wai-wasmq/src/meter.rs`). ### 3.3 Refusal codes | code | the module or envelope | |---|---| -| `W001` | is not a valid core module in Lime1 (a component, a malformed module, an instruction no other rule names) | +| `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 shared or 64-bit one, one without a maximum or past 4096 pages | +| `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 | | `W004` | has a start function | -| `W005` | has a shared memory or an atomic instruction | -| `W006` | has a vector instruction, relaxed or not | -| `W007` | has a reference, exception, tail-call or garbage-collection instruction or type | +| `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 locals and operands can pass 4096 | -| `W010` | is larger than 16 MiB or has more than 2^20 functions | +| `W009` | has a function whose parameters, locals and operands can pass 4096 | +| `W010` | is past a size the last rows of §3.2 give | | `W011` | has a data or element segment that does not fit where it is placed | -| `W012` | exports a name the profile reserves, or a lifecycle export (§5.3) with the wrong type | +| `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 (§2.3) | | `W014` | (envelope) has a frame whose digest is not the one its contract pins | -| `W015` | (envelope) has a callout whose module does not export it with the type its `extern` lowers to | -| `W016` | (envelope) has a program the reader refuses, that calls nothing, or that is too large for its method | +| `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 the reader refuses, that calls nothing, with a gate outside §5.1's, or of more than 32 768 qubits | | `W017` | (envelope) has a run outside the profile's ranges | +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 @@ -196,8 +217,8 @@ maximum before it calls the instance (§4.7). ### 4.2 Fuel -Every call into an instance is given a budget of fuel. Fuel is defined on the module as -written, not on any engine: +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`, @@ -215,51 +236,82 @@ written, not on any engine: 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. These are the pinned native engine's default operator costs, with one for -a byte of `memory.copy` or `memory.fill` and one for a page of `memory.grow`; a test holds -the profile's fuel equal to that engine's own count, on the original module, over every -kind of control flow, call and memory instruction (`wai-wasmq/tests/engines.rs`). +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 one 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 a +`memory.grow` that fails, the profile charges the pages asked for and that engine's own +meter does not (`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 Call depth +### 4.3 The stack -A host call starts at depth 0. Each `call` and `call_indirect` raises the depth by one -before it calls, and lowers it by one when the callee returns. A call that would raise it -past **1024** stops with `call_depth_exhausted`. With the frame rule of §3.2, a call within -the depth holds at most 1024 frames of at most 4096 slots on any engine, which the -reference engines hold; so no conforming module meets an engine's own stack limit. A test -runs a function of 4000 live locals at the full depth on both engines, and shows that the -native engine's limit, set to 1 MiB, would have stopped it (`wai-wasmq/tests/engines.rs`). +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 + the bytes of its entry in the code section +``` + +its parameters and results those of its type, its locals the ones it declares, 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 their bytes allow (thousands of values held across a call, a new value held at +every step of the largest body, 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 60. ### 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). Any other engine runs the **prepared module**, a pure -function of the module's bytes that keeps every instruction byte for byte and adds: +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_depth` (i32); + `__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 the page count, each taken from the operand with `local.tee` of one added `i32` local; -4. around each `call` and `call_indirect`, - `global.get D; i32.const 1; i32.add; global.set D; global.get D; i32.const 1024; i32.gt_u; if; unreachable; end` - before it and `global.get D; i32.const 1; i32.sub; global.set D` after it; +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; -and drops every custom section. The host sets `__wasmq_fuel` to the call's budget and -`__wasmq_depth` to 0 before each call. When the prepared module traps at `unreachable` -with `__wasmq_fuel` below zero, the fault is `fuel_exhausted`; with `__wasmq_depth` past -1024, `call_depth_exhausted`; otherwise the module's own `unreachable`. +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`. -Because the preparation meters, counts and canonicalises inside the module, any -conforming WebAssembly engine runs a prepared module with the same results and fuel. The -prepared module is itself a Lime1 module; its digest is in the receipt (§6.4). +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 @@ -277,8 +329,9 @@ 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, threads, tail calls and every proposal past Lime1 - off, and its own fuel on as a backstop under the profile's; + and refused, vector instructions, tail calls and every proposal past Lime1 off, its + own fuel on as a backstop under the profile's, and each call run on 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 runs the prepared module too, with its own metering off. @@ -311,9 +364,17 @@ 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); 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. +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 @@ -331,29 +392,50 @@ declares them: - **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 host allocates - each argument with the module's `cabi_realloc(0, 0, 1, len)`, writes it, calls, reads - the result, and calls `cabi_post_(retptr)` if the module exports it: the - Component Model's canonical ABI. + `i` in byte `i/8` at bit `i % 8`, the rest of the last byte zero: the Component Model's + canonical ABI. -The host charges the call one fuel for each byte it writes or reads (§4.2). A result -whose bits pass its width, a list result of the wrong length, outside the memory or -not 4-aligned, ends the run with `bad_result`. Every guest function the host calls for a -callout (`cabi_realloc`, the export, `cabi_post_`) shares the callout's budget. +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 When an instance is made (each shot when `state` is `shot`, once when it is `job`), the -host calls, in this order and each if the module exports it, each under the instance -budget `per_instance`: +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; + contract's order, the bytes passed as a list argument is (§5.2, step 1); 3. `init()`. -A module with priors MUST export `wasmq-prior`. +A module with priors exports `wasmq-prior`, `cabi_realloc` and its memory (`W015`). ### 5.4 Priors @@ -363,14 +445,22 @@ a module is never run against another prior than the one it was packed with (`W0 ## 6. A run and its receipt -### 6.1 Shots and randomness +### 6.1 Shots and outcomes -A run goes shot by shot, in order. Each shot starts from `|0…0⟩` with every bit 0. A -measurement of a qubit whose outcome is 1 with probability `p` reads 1 when -`((x >> 11) + 0.5) / 2^53 < p`, with `x` the next value of SplitMix64 seeded with the -shot's seed XOR `0x51574d31`; the shot's seed is SplitMix64's output for the state -`seed + (shot + 1) · 0x9e3779b97f4a7c15`. A shot's outcome is its program bits, bit -`n − 1` first, and the counts sum the shots that read each outcome. +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 bits, bit `n − 1` first, and the counts sum the shots +that read each outcome. ### 6.2 Faults @@ -378,53 +468,76 @@ 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: `call`, `instance` or `run` | -| `call_depth_exhausted` | §4.3 | +| `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 | -with the shot, the phase (`seed`, `prior`, `init` or `call`), the module and, for a call, -the callout, and the fuel the stopped call used. 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. +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. +receipt that recorded them would differ by host. Nothing a bundle holds can lead here: +what a bundle could cause is refused on reading or sealed as a fault. ### 6.4 The result -The job body is the engine's (`engine::job`): `{ "status", "method", "request", "result" }`, -`status` `done` or `fault`. The request is `{ "wasmq": , "envelope_blake3" }`. -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; `status` and `method` only repeat what is signed, and a body +where they differ from it, or that holds another member, is not a job of this profile +(§6.5). + +The result has exactly these members: | member | | |---|---| | `format` | `"wai.quantum.wasmq.result/1"` | -| `profile`, `status`, `method`, `qubits`, `shots`, `shots_run`, `seed` | | -| `counts` | outcome to shots | -| `callouts` | calls made | -| `fuel` | `{ "used", "instances", "budget", "callouts": { name: { "calls", "fuel", "max" } } }` | -| `modules` | for each: `{ "name", "blake3", "prepared_blake3", "memory_pages", "floats" }` | -| `transcript_blake3` | BLAKE3 over `wai:wasmq-transcript\x01` and, for each call in order, its shot (u64), callout index (u32), input length (u32) and bytes, output length (u32) and bytes, and fuel (u64), little-endian; after a fault, `fault`, the shot, the reason and the fuel | +| `profile` | `"wai.quantum.wasmq/1"` | +| `status` | `ok`, or `fault` when the run stopped | +| `method`, `qubits`, `shots`, `seed` | the contract's method, the qubits the program declares, 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` its calls, their fuel and the most one of them used | +| `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", "note" }` | -| `energy` | `{ "unmetered": … }`: the runner reads no energy meter; fuel is the measure of work | +| `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 | -Nothing in the result depends on the host or the engine, so the result's bytes, and the -receipt's digest, are the same everywhere; with the same key, the whole job is. +**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 -`wai-quantum verify JOB.json` checks the signature, that the result is one this profile -writes (fuel within the contract's budgets, each callout's largest call within the call -budget, the modules the contract pins, a fault reason §6.2 names, counts that sum to the -shots run), and with `--bundle ENVELOPE.wai` that the envelope is the one the receipt -binds and that running it again here gives the same result, byte for byte. +`wai-quantum verify JOB.json` checks, and exits 0 only when all hold: + +- the receipt's signature, by the engine's own check of a job receipt, and it names the + **signer**, the key the receipt carries, as a `did:key`. A receipt shows that this key + sealed this request and this result; it does not say who holds the key, which is the + reader's to compare with the signer it expects; +- that the body is one of this profile: its four members and no other, `status` and + `method` as the signed result gives them, and no whole number past 2^53 − 1; +- that the result is one this profile writes: fuel within the contract's budgets, each + callout's largest call within the call budget, the modules the contract pins, a fault + reason §6.2 names, a run that is `ok` only after all its shots, counts that sum to the + shots run. + +It prints how the run ended. With `--bundle ENVELOPE.wai` it also checks that the +envelope is the one the receipt binds, and runs it again here: the result must be the +same, byte for byte. ## 7. On hardware @@ -432,22 +545,15 @@ binds and that running it again here gives the same result, byte for byte. 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 cannot carry, so `run ENVELOPE.wai --provider NAME` is refused, and the hosted +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. -**No provider's machine runs a wasmQ module in its control loop today.** Real-time -classical compute beside a quantum processor exists: one vendor's control system runs -WebAssembly modules on hardware modules next to its processor, called from its programs -through its own interface, with integer arguments on one system and integers and doubles -on another; another runs native functions on co-located accelerators over a network -transport, with a timeout. Neither, in what they publish, fixes a deterministic profile, -meters the callout, binds the binary by digest or signs what ran. +**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: -What holds for a module on such a machine, and what does not: - -- A scalar-form module (§5.2) is a plain integer function with no imports, the shape - those WebAssembly environments take. Whether a given machine accepts it is that - machine's to say; this repository has not run a module on one. +- 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 @@ -474,9 +580,9 @@ checks in `CssCode::check_matrix_bytes`'s form. Exports: `decode` (list) and `de (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 stabilizer circuit) with the -decoder called between the syndrome and the correction, each shot ending in the code, on -every engine with the same bytes. +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 @@ -493,16 +599,27 @@ feasible fills the target, and atoms are moved, never made or lost. - `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; + whole job's digest. Among them are runs that end in each kind of fault, a callout of + the list form, 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`), the modules also as WebAssembly text; + (`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; - `modules/`: the modules of record (§8). `wasmq_vectors verify wai/quantum-conformance/wasmq` checks them on every engine of the -build and prints a digest of the outcome, the same on every host that conforms. +build and prints a digest of what it computed, 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 lowering, the transcript, the result's -members) is a new profile version, and a receipt names the version it was made under. +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; +- 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 index efccd4a5c..e46134c71 100644 --- a/wai/extensions/quantum-wasmq.wit +++ b/wai/extensions/quantum-wasmq.wit @@ -5,7 +5,8 @@ // 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. +// 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. @@ -24,7 +25,7 @@ interface receipt { integer-overflow, invalid-conversion-to-integer, fuel-exhausted, - call-depth-exhausted, + stack-exhausted, bad-result, } } diff --git a/wai/quantum-conformance/wasmq/README.md b/wai/quantum-conformance/wasmq/README.md index 8d17c0070..43aafe328 100644 --- a/wai/quantum-conformance/wasmq/README.md +++ b/wai/quantum-conformance/wasmq/README.md @@ -3,15 +3,15 @@ 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 in -that file. No media. +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 and its sealed job | -| `refuse/` | modules (also as WebAssembly text) and envelopes a runner refuses; `vectors.json` `refuse` gives each one's code | +| `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 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 0000000000000000000000000000000000000000..7b2a00607d9316b62cbd15729158041984be47ef GIT binary patch literal 845 zcmZWn&2G~`5Vni>k$DMgPgDxQ>z~v~M5Q8e;839A&_h*~S$j5JYOkHmT5Yf#ap28( z0A7h%rxXyrc;=gLX1<-7^OJY{hv#`cy9LD-j87vrVKtTt$P-F~*%F|%MQsT&*jB0l zi;mTlSX~)hcgkQr#(P#k3qn=OZi!7br|S;1?ds_*Z0a>-NvJ9T#SL5NuBwI!D;ixY zs~WBJiYB9@S) zG0Nm9j(M8K$t;&;#%Cb2GA<-0X`W`YNaV8&VTNaSOYjKGyvXu7h!TYa%+rKp90`$=&?Cu{IcX9Jlsw{6 zWKk5wA_dNY=Lw(xk;Y@1snYE@uzsMan~p}*GudI~{w%R)CQRv#1znKt z-}rstOC$cmTmluX(Ox%hxs>FNuN-pxt!|>&CxHp%t(RVLh)dkWpF(P!q;Jq wu;~ms*N{ua4wr#Pw@X*$`A_`M{?6Y$I`F-xk9}WyUJ&@tywH2|?cn?KUoT7kivR!s literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-depth.wai b/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai similarity index 100% rename from wai/quantum-conformance/wasmq/envelopes/fault-depth.wai rename to wai/quantum-conformance/wasmq/envelopes/fault-stack.wai 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 0000000000000000000000000000000000000000..d696919ebc676a3872d8ca2b72d48b7e60c0dbed GIT binary patch literal 849 zcmZWn&2H2%5Kf8!Dew@HPgGhKJ3rlIS0ELM1BU_?haRe`jFZXMG;y+CM{S8xfp|3@ zfJb7SEmaYbtjv7#&HtPnz8Akd&l}hkD7Iu`&e@DLSSlb*Dh=j_0HrNzONha)Rs~pe zthU7Z+~B5H2Ajz~umU=es#f-f*c5ZV=|S7RnP0)S*^pNSbuFQ|WJ}%G^*Ccit4n27 ztCe1}<-jhLE+=2dC%Xl+M#?>~O)r(*vSl`7+l$so2MLLY#81ZY$DZe%93P3_zU!x; zJZom)I?t+Tk`R2>WGw`i>%=iV`7yegFK1zG>3$z9Mc6C zX&e<0@dQxtQbcLad3mtNii0Y`EQuwE8~_%JJS}p{C~vHbnurtk5ZW2l8kg+M zalkJ4$Zk6DO(B(;yx;wFr~K^bU}6h z#qSJXTloNU4ivOOd(pb>l9RiVZ)m8syAA%kRqKw7h_q-njvqh0`*3{t`6!I%d=)5N z)P0E|g(=s>+s+ALt6;a;uzEpva+;m(hcDlRJ8>0E$3tMaSp_>Rl=qTZ1s9Vcd>u9j w+uop44XG4te-(Iiy>wQd|HS{|-}<+Y_I&T@W8belFL>?;-djJU_w)O+KTN0ob^rhX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..851f188e1dd929de6f911e0a5cd0b73dcc6710c2 GIT binary patch literal 910 zcmZWoJ#X7E5S5jrEt~?)(lLO*sR2@#v_5FrXaNr$I;3sTAwv)(Bz1DwNR&)VNL<56 zhW@Tj-TRk%l-)0s1I2rMclUUAI-9;@-#yP;kz1k3n6LpQ$E1Wz3$&y*!Y&cerUBC+ zVxcay5)C+3l|gZ7Vcu#BWp}?KO4LH?LN`lfW6Z(46{cy+!L3-7bM(qZQAnX~$k?<+ zu|6iMGMR34Wwe=*@rvANlXbq1kN1tRGr-&{GH<1B7G%ta$7FF`x#I{=0LTy5>uVrA3gUTg?Xyhan4r+?&``q{W&Uty9TV;8u*B|05YcQi|$2eY+Pg^ZXiTfNSR3a zkkTkt$tYzpjVOyGPk0*gPzfQ@n5&4#f(e<^NG3xShrpvzLKT%UW|YguMNLty5hZ3L zC8D*}m37H3T<+Rdxpqqaui7itv8$OCrK`hlX)I&;D5W$3ISNA<(j*s2#&|Rg^Eiv+ zRHYnJHi~mDCCFHC7I8=-&eIG8bz!&CSQxr4*ImI@Yb?8Mu|zA9w@|om4ffKNl#ChO zER>9xHj73Wg}Q&?baU~!l7C^&1%sB*Tvx8U=;Ws43s}^!?chI6ZQP6@58F)E>kl8_ zzCWFQItju7o%FR)MVmo@QL-8G76U}cq`zrwkiWn&Ip@QRqu}M6V0)hQyXkRoFrW0d zXdwR_?WBL*T?Ma$62zjl;7p4qMm(DIJ)B@1EwA^?zuLXW)w{p)-M+Kqd%eBg9Jdm>mT}i-t^(g{&U<90x$8s>DPmx=e`dI`#%A3F$=^1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f17fb80236fc2f9085cb88e714ba7faaf541fe5 GIT binary patch literal 877 zcmZWn&2H2%5KamdDZE7F6P1?5-u&!lS0MGkfkT0cLl0F|Y-hY{nmAdWG*p!k9siD*yJ)sS^w+LwCz&J!Kw3U|J zfoC-(RF@XEy|z&A;scSq}&A+I3`EOh~tByp^YD0wABUar}7@1ZSrw?04uN zB(YJ`d)E0t;HGV?e}KXf;KqS9_YS_IuYfG+c(HpDI{Oq+@heD>C3#8#L_SZ`LUFoC z*doh{d?x6er3sWp$v9IpktXGgmI~QSS52}BF}%iZq(-#0y0O05 zh40&48w9 z#&af9#`7$f3yjRwKh{_^3|>G9W-IGNH_ zq>Zfl5@M`Vu94SG5h1H+zuH*6#!ES0TpY!3-p6}$746Ok!FYRb$9xs-gRywD>?+#$ zfQ1g+lYu!KpD5yYaSgodEqK?Wi_MN!QGoZ2cNc_*;kWQEynA*K1}|TPp$dZNFnk`w G!K*)^`3Hjl literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json index c2def3df0..2109b79c8 100644 --- a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json +++ b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json @@ -89,19 +89,12 @@ "modules": [ { "name": "qldpc-decoder", - "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962", - "prepared_blake3": "20b25f014aad7adae6257af37b1350eddc06ff9de09d87578a8e13d9afd93e61", - "memory_pages": [ - 17, - 1024 - ], - "floats": false + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962" } ], "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", "accuracy": { - "class": "exact", - "note": "each shot runs alone, its measurements drawn from the run's seed and the shot's number; each callout runs in a metered module with the same results, fuel and fault on every host" + "class": "exact" }, "energy": { "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" @@ -110,8 +103,8 @@ "alg": "ed25519", "canon": "rfc8785", "key": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", - "digest": "2013608fef63aa4f46f50e8656fd35e1bbe673b18e6a18ffb2dbbf315332de06", - "signature": "1e5faffea8155469e251891e1a846ad178225b1ec96fac1aa49627bb34bd842570aa688cbea54abb54744c01158b65ef36d44ad4688baff830f141ad491e4104" + "digest": "3c5ed83161d26ddd654aefb76291c823a12ad1c7218dfe7f435c14ec91dbeab2", + "signature": "9d7699445906d02dba7c2c088fedf4b6fcec8b1db029307194b2f37ac0c97cfd9b1bde68b845dd9158490995d7f56983c161a95541410c3e2b1d62de55240c0b" } } } diff --git a/wai/quantum-conformance/wasmq/refuse/atomic.wasm b/wai/quantum-conformance/wasmq/refuse/atomic.wasm index 0dec7b4b6752a44a23cb4392c7e806a840f2de94..e562c5f8ac6021483b1f2d9ab2b77822cd0fb7de 100644 GIT binary patch delta 38 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a7e8144cac322b4dfeda9e6e0a1adca6d84d863e GIT binary patch literal 841 zcmZWn&2G~`5Vi{x$-IQMCn|;D^(Kv-2&5i3a41l5=%K30{>-LJ?X|O6t4%Gd561)W zO3XT?fXKm_`R1FMZ)fKGk)C#Rz`9U zfz>Fet{iMSuAAEm;;5vb}DCHA4#k>2w(G52EP&?3DhD!*e9I zX}Z9<=n34ljSKQM3V=Hg&OSQ$hOPp#%!bSHLFnvLM8$7_A?ujUm?#&;Y$1i3QATw! zpU)L%IX})+#)M?}<%<$>QOL5Sd0t4)^)VDG11+(m*!fVE%_qe28oQAi(bk#9g=Uwb zck3E$f|CD-_Lh$9X6<+##_%MK!2(nv=S9hd0--q+Ge#lLgeXL=7n-s%ms&8cXhyY| zXIYku8K;!fg3uJiS*(xzs zR-J+rtEe^dwo^pND%meK)vxhNE{e;e^v(NpZ?2N@bP!B8t7MO*;>mKWAI9I}hxp;yK^(n&5yv`;lEe6Ult!<90n88mlK=n! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d6caea0e53a6dc2c5bf87a0ccd928850f6fbe0a GIT binary patch literal 928 zcmZuw&2G~`5Vl(cB7F&KPpuTfIvLGC$wUd^_{y^yp*w)APKJU4v#T7S1@EvJ#~R@`N^EZz-TngQlT~#k$ZL zH0V&3#Nxu@rqve9A>XkKY7n~6%`Ks+=WNr0Y1(pj4cl@>m0+?AF^wh0Nt|IA$8nNz6{1=sa{&t)B5oUWZ;GePKuM%n zNzvNs%DQ3aJsBlx|y3)M8W^ahVX2rJ9Sm|ezyPjMQiQrvOB5YfW&b8#ypo7uM_bpi-{Sio_p8wMS=HK~uySpUZSFd+`MX}uT OCwn_DyujNVzWx9yF&78` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e4bdb6f2247621e9f270f615edd9723ce883de14 GIT binary patch literal 41464 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 I)}`|Q8v>+2a{vGU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a241320270edb579e14ec6e6f28d1538c80e77c GIT binary patch literal 41455 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 E9~L1!g8%>k literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai b/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai index f5f3cae354ff9f9ecff7316d90c498e9486aa758..54501e5c3f79908c4575c3d78bfa922f2cc1c19e 100644 GIT binary patch delta 21 dcmaENnCabNrU?fbjW!-iWMVYltipV19sqD!2{!-$ delta 21 dcmaENnCabNrU?fbjW-@jWMVYhtipV19sqD)2{!-$ 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 0000000000000000000000000000000000000000..a42447e417bc45a6e57c2ef225b5374e475c7974 GIT binary patch literal 27 icmZQbEY4+QU|?WmVN76PU}j=u;9_TFWo%Ta;|2gKTmta` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4f3432c87a45b0c3b7a5a1fdb5ebe3c6b03288df GIT binary patch literal 24 fcmZQbEY4+QU|?WmVN76PU}j=uzz%O+G~5WRvRq$z^m~9 zyb`lXDIk3D&Ntu8eDia4{yzTUdEP*7nIcOPPa`rRH3-Gf;z~2KM?h%{+9G10tyIY@ zI94N|x;D`D%0PYa52R!*%y1a;g`mUW4 z%QU^?tOf#iZDX8#i2~rxg3%8K_Rv>AmeIIAG=#=6gjehaD6&eHL;yU?lVl-TG^g=A zO&8gWM|qr35DO8rSk8Dt#VisM*|E$x@CBc9nSmQ8-&>cJrW2xBjnznvXlqnsT(PyQ z-I_)!r{w>ky^as;W@A|G#_%XjaGJ11o+k4&TF6pnGss=(l!*jp93-U~m3a=3MM;^( zIb~_iumb@?iZuTtEvA`Ua(fuqFyhork0Tn0?4ff17RE4Gd~{*x)})weG}_hifL|`0>-b50~ekFG4zvia_bI z>IHl`-hR?!;a w@J(0)+w}$4vM0Ani!s;n@!m4C0qotAUB{Je$ugUec3ZnsiYEZ1(_t7+Jn!oAg8uZ~a|&!z zcaC%26S!|`CmE{$PP;PZ@}C*OVMd8;y#p&n9{9fx3PAa|I`b;7ry{`yZ-k8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f5f3cae354ff9f9ecff7316d90c498e9486aa758 GIT binary patch literal 41454 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 HRQ~?}36woz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..547a836b9fda4863eed4a69370ed2f71f0407b48 GIT binary patch literal 901 zcmZWoO>fjN5KYonpcIK7;8-d0iAu{7Z<0;2E0FfUfkT0cLl0He+R1opnmAc+qP9d) zd*Cne54iJ}7-yF+;e+FOelzntGdW$nqu(vd>c|a~WJc)NCnHiqE*V-}DyHuckg5UI zAfllzWX2jWtSX1%Lc^++8p`3mBN?k1mxXNZkc~0NtCp#zEyp)(U9Qkeu%h5ByCyT$ z7DYcIS*3E>$Vy4IB(siOOO+44hL86R(@VhI9a*(pHfu7ACnK`Hs?0IO69DpQ-+yx~ z>-6N9{;YOu!d>8l?mX$Wi&Q0!GS3(*k|5xo59oW@Uv(nVzw=|klk)*yq1)c-}O#G=})11cPBoI*^ zMQN7CkkTX)G3S7?V;aUV4dXb?f%zuvRvHZg)8%?7Sl46Ob&DnHh-jfO-x~CVDJfRk z-z=1jm{jYAsSI`h!u`#~=ZgP@85ay%LUUD_?xK^Kk}sf9!?uI}G_^4^hCFOD>H7~K z-@ZRtd^+}mu|IdE%8E7z52NHumLn(7?jZ%E*|;z`(`E$ikq&zzqO1k^<5I diff --git a/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat index aa606182f..41d6c68ed 100644 --- a/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat +++ b/wai/quantum-conformance/wasmq/refuse/memory-too-large.wat @@ -1 +1 @@ -(module (memory 1 65536) (func (export "f") (param i64) (result i64) local.get 0)) +(module (memory 1 4097) (func (export "f") (param i64) (result i64) local.get 0)) 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 0000000000000000000000000000000000000000..e4f010d5dd97df2959d1220d1cc7aed30da45754 GIT binary patch literal 841 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 E0119QVgLXD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..af1eed9c5480a6a5ef8989f246f551e40e54cc26 GIT binary patch literal 846 zcmZWn&2H2%5KamdX?cmrCy9*zg# zl^ADBp(2;eeDlqZznSxs5A0_U1Uj)L_As%|@!s3)p}%KuxiS6)(7njStJ88K~kj++wa{)5v) z;9Dd9!F&o9t-);?zg$f6NAevk&TzlMM@x17#E^$;Cd2UQ^ZSoyCtpsZWJXsLZOW>X z5MdR$M&53Q2w6=Ii;dJvypoIj@>TTuU34&4lks#AjJB)E0gJ?gWml8U7>M3RHSk?$ m!Mpac8{h%sX$9eN_%*x>@17im!SiQfsDfZ}96k-A;KgrZ@&GRY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ccc7ce6dcaceba4582091c0588d2cbad0d44ad9 GIT binary patch literal 49 zcmZQbEY4+QU|?WmV@zPIW2|FlVq{=pVJu)^WMyFl0(MqL#xw>7E)GUEh6@E3*Kz{@ DjAsUn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0bb2fc816d62956c34807d995012b1de60813306 GIT binary patch literal 43 xcmZQbEY4+QU|?Y6VoG4lXGmbIW2|FlW@2DuXJuqe0}698F)?tnFeor^0|0v31Wf<{ literal 0 HcmV?d00001 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 index 7177b2cb9d213b6ce13987e109f9cb688e62ecea..4a5c3a0938f6eb309ebc1bd40c41acbf1d24df3e 100644 GIT binary patch delta 48 vcmXrEpCF>m!opaM9^hI$VzZp|KAU6~BQLStZXX zfS50`tWcchDa&WGVx9=BpQRuR$v9Jq$Wob5sgTXoT!1J1(&PQQ9l{cZRRo$Zpn_*M2IG(o$G}%*$C`a3Mh`4tbU`NT|TtiCQR@ z7D=f@$|XZpVn!)Np`4*&?4$TCjU_l@ZQ5~Q{eVlb9ge6cs)Ne^Td-Gtq-4o3iT_YC zV%lsSHzn%+g+DmHG~y4;2Vl|~+;!u3jY0lqzJf&!|6k}|D|h|?F$|A@48w5KMO+@1kqvmG>C&2KLPNP B1S$Xk literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b430923676b2cf1de4fc1d1df37b190d98098400 GIT binary patch literal 56 zcmZQbEY4+QU|?Y6VoG4FXGmbIW2|FlW@2Du7h>Wn2TB&|7N@4B02QPG)o?N~F>tdm IC@^pX00@x?YybcN literal 0 HcmV?d00001 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/simd.wasm b/wai/quantum-conformance/wasmq/refuse/simd.wasm index 94907dbb5868f110997be1629d6b4358184c9772..96e9d20e70616bf0d2afd218091e1d1b40a1c976 100644 GIT binary patch delta 37 rcmdNenINLT!opa@DyK?DE?iUGy| diff --git a/wai/quantum-conformance/wasmq/refuse/simd.wat b/wai/quantum-conformance/wasmq/refuse/simd.wat index 78496c470..01582c310 100644 --- a/wai/quantum-conformance/wasmq/refuse/simd.wat +++ b/wai/quantum-conformance/wasmq/refuse/simd.wat @@ -1 +1 @@ -(module (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat i64x2.extract_lane 0)) +(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/table-grow.wasm b/wai/quantum-conformance/wasmq/refuse/table-grow.wasm index dd3470bf8e78271a00b5848cb41e92ff14683f2d..d5bc4532bd105c65c2934c9fc692a70532c92098 100644 GIT binary patch delta 29 jcmdNlnjj^?%EAZ)?5vE8X$%ZpoQ&)Y41f3;)^Y;?E>;7B delta 26 hcmXrgpCBd5&dSJ`#=yYE!^p*Op}>*x4?n|NZU8tJ1p5F0 diff --git a/wai/quantum-conformance/wasmq/refuse/table-grow.wat b/wai/quantum-conformance/wasmq/refuse/table-grow.wat index 624a3974c..a3e2dc7f1 100644 --- a/wai/quantum-conformance/wasmq/refuse/table-grow.wat +++ b/wai/quantum-conformance/wasmq/refuse/table-grow.wat @@ -1 +1 @@ -(module (table 1 funcref) (func (export "f") (param i64) (result i64) ref.null func i32.const 1 table.grow 0 i64.extend_i32_u)) +(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 index dd8244daf90a731d4cd54e00a21e3cc97d6c96db..69215ef83eca3e40af5e286679191500858d94e2 100644 GIT binary patch delta 28 icmdNkoFFC6%EAZ)?5vE8X$%Zp9E@xXe*_rTasvP>=L38I delta 22 dcmXrio**U2&dSJ`#=yYE!N|t&M}T21Hvl8F18D#N diff --git a/wai/quantum-conformance/wasmq/refuse/table-size.wat b/wai/quantum-conformance/wasmq/refuse/table-size.wat index 8b9b52069..851c26fac 100644 --- a/wai/quantum-conformance/wasmq/refuse/table-size.wat +++ b/wai/quantum-conformance/wasmq/refuse/table-size.wat @@ -1 +1 @@ -(module (table 1 funcref) (func (export "f") (param i64) (result i64) table.size 0 i64.extend_i32_u)) +(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 index 4835c958ed9ed8ce834e602c6177dce7edca78f1..f84e7c040230afd0c572c16ea4164e975b63110e 100644 GIT binary patch literal 62 zcmZQbEY4+QU|?WmV@zPIW2|FlVFvQpSs5AA7?`+(m{}MU7`WMhgb)Kam<^=z5_3}- JSr{1@(*eUx1_uBD literal 56 zcmWN>F%m#96hOiEl8709G)~}XQlN@PpR=>Y(%vp$^>8?HPB2e;;j*T}PSn4RAj;GF HX%X)astX0r diff --git a/wai/quantum-conformance/wasmq/refuse/tail-call.wat b/wai/quantum-conformance/wasmq/refuse/tail-call.wat index c604c258e..4b2cae8db 100644 --- a/wai/quantum-conformance/wasmq/refuse/tail-call.wat +++ b/wai/quantum-conformance/wasmq/refuse/tail-call.wat @@ -1 +1 @@ -(module (func $g (param i64) (result i64) local.get 0) (func (export "f") (param i64) (result i64) local.get 0 return_call $g)) +(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 0000000000000000000000000000000000000000..f278f491cabd76179b429d2dc33fae1519e1492f GIT binary patch literal 7119 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 literal 0 HcmV?d00001 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/two-callouts-for-one-extern.wai b/wai/quantum-conformance/wasmq/refuse/two-callouts-for-one-extern.wai new file mode 100644 index 0000000000000000000000000000000000000000..c563902949708060109bd0d5a20c40f50cba4d8a GIT binary patch literal 880 zcma)4&2G~`5Vi{x$-IQMCn|;DwVlLHM5P`$a41l5=%I?ru6H(BVy~UeT5Yi0ci~xh zC(g_|p@6vHi+8^HX6Bop>x*~vyXScwxnqjV2^~jdL@E%9p~aPEW{-f<7PLjgKvSxM zS#YdcKzVCm+bRRq;O|Jm8pc(r>>k;ebG&Vtwrw@OW4mgLQIeGb8nT&{L6^bGiqK#3t zamAKB)^1EKl@t8$Y+uuXU9SwQ+_WC0B`^b#6_YGyTmY90vNWL(M;vR%awch#$A#nx z6BJePNfbpWlu=ZSedNET(HKXsbTiDg>v1`z#YuKVwotl%1LoFE9v6a1+=r48Q+j8a zE>QO`{OI^n^FJ^bfJrN`>)PD_2Dw}M3I;VC7yZ}nt-D0z;Wd!H|M2nc`>TsjmtiuF z7J<@5*$N0Ti&!CVH%5dkg2Vnp`4W%mCR?6`ug=56a}f*|hG4i|1P3hSk1w+b)`KH_ v6IQ@>tpR5mG%?xPBJl9U@%TLd)c@>1_zzD`eDC=)- job.json # each call metered; the same by wai-quantum verify job.json --bundle round.wai # the receipt, and the envelope run again and compared ``` -A call that runs past its fuel, recurses past the call depth or traps ends the run in a -receipt that names the fault and still verifies. `run --engine native|interpreter` chooses +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` names the signer +and how the run ended. `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 and checks each call. The quantum part is simulated here: an envelope goes to no provider, diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index aba77d0af..372847159 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -1907,6 +1907,10 @@ Options: 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 @@ -1948,9 +1952,6 @@ Options: [default: 100000000] - --input - A value for the program's integer `input` (repeatable) - -o, --out Where to write the envelope diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 3f29eb10c..5e99e2520 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -1196,7 +1196,6 @@ fn main() { let _ = INPUTS.set(cli.inputs.iter().cloned().collect()); let _ = FROM.set(cli.from); match cli.command { - #[cfg(feature = "providers")] #[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")] @@ -1208,9 +1207,10 @@ fn main() { 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), + 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 = "providers")] 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)); diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index 5b3317020..1d8a00bb9 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -20,7 +20,8 @@ fn read(p: &Path) -> Vec { } /// Why a bundle goes to no provider. -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: no provider runs it, so it runs here. A provider that runs WebAssembly beside its processor would need the profile's engine in its control loop; none does today"; +#[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 { @@ -60,9 +61,6 @@ pub struct PackArgs { /// 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, - /// A value for the program's integer `input` (repeatable) - #[arg(long = "input", value_name = "NAME=N")] - pub inputs: Vec, /// Where to write the envelope #[arg(short, long, value_name = "FILE")] pub out: PathBuf, @@ -85,7 +83,9 @@ fn module_name(p: &Path) -> String { p.file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string() } -pub fn pack(a: &PackArgs) -> ! { +/// `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(&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(); @@ -109,17 +109,16 @@ pub fn pack(a: &PackArgs) -> ! { let bytes = read(Path::new(prior)); modules[module].1.priors.push((module_name(Path::new(prior)), bytes)); } - let mut inputs = wai_quantum::qasm::Inputs::new(); - for i in &a.inputs { - let (k, v) = i.split_once('=').unwrap_or_else(|| die(&format!("--input {i}: give NAME=N"))); - inputs.insert(k.to_string(), v.parse().unwrap_or_else(|_| die(&format!("--input {i}: not an integer")))); - } 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())); - std::fs::write(&a.out, &env).unwrap_or_else(|e| die(&format!("{}: {e}", a.out.display()))); let o = wai_wasmq::open(&env).unwrap_or_else(|r| die(&r.to_string())); - let (h, method) = wai_wasmq::envelope::read(&bundle.program, &bundle.inputs).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() { @@ -139,7 +138,13 @@ pub fn run(file: &Path, json_out: bool, top: usize, key: &wai_quantum::ed25519_d }; if let Some(path) = &a.replay { let text = String::from_utf8(read(path)).unwrap_or_else(|_| die("the transcript is not UTF-8")); - let unhex = |s: &str| (0..s.len() / 2).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap_or_else(|_| die("a transcript's bytes are hex"))).collect::>(); + let unhex = |s: &str| { + let digit = |c: u8| (c as char).to_digit(16).unwrap_or_else(|| die("a transcript's bytes are pairs of hexadecimal digits")) as u8; + if !s.len().is_multiple_of(2) { + die("a transcript's bytes are pairs of hexadecimal digits"); + } + s.as_bytes().chunks(2).map(|p| digit(p[0]) << 4 | digit(p[1])).collect::>() + }; let calls: Vec = text .lines() .filter(|l| !l.trim().is_empty()) @@ -192,7 +197,7 @@ fn print_job(body: &Value, top: usize, kind: EngineKind) { 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 {}, prepared {}", m["name"].as_str().unwrap_or(""), m["blake3"].as_str().unwrap_or(""), m["prepared_blake3"].as_str().unwrap_or("")); + 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") { @@ -215,11 +220,21 @@ pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { let verdict = wai_wasmq::verify(v, opened.as_ref(), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); println!("signature {}", if verdict.signature { "VALID" } else { "INVALID" }); println!("binds the contract (program, modules, priors, run) and the result: fuel, modules, transcript, fault"); + // Who signed, as every receipt's check names it: a receipt shows that this key sealed + // this result, and whether it is a key the reader expects is the reader's to compare. + if let Some(k) = &verdict.signer { + println!("signer {}", wai_quantum::engine::job::did_of(k).unwrap_or_else(|| k.clone())); + } + match (verdict.outcome.as_deref(), v["result"]["fault"]["reason"].as_str()) { + (Some("fault"), Some(reason)) => println!("outcome fault: {reason} in shot {}, after {} of {} shots", v["result"]["fault"]["shot"], v["result"]["shots_run"], v["result"]["shots"]), + (Some(o), _) => println!("outcome {o}: {} of {} shots", v["result"]["shots_run"], v["result"]["shots"]), + (None, _) => println!("outcome not stated"), + } if verdict.well_formed.is_empty() { - println!("result a result the profile writes: fuel within its budgets, a fault it names"); + println!("result a job the profile writes: its body agrees with what is signed, fuel within its budgets, a fault it names"); } for p in &verdict.well_formed { - println!("result NOT one the profile writes: {p}"); + println!("result NOT a job the profile writes: {p}"); } match verdict.bundle { Some(true) => println!("bundle the envelope the receipt binds"), @@ -236,17 +251,18 @@ pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { pub fn check(module: &Path, json_out: bool) -> ! { let bytes = read(module); - match wai_wasmq::profile::check(&bytes) { + // 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 prepared = wai_wasmq::meter::prepare(&bytes).unwrap_or_else(|r| die(&r.to_string())); let exports: Vec = f.functions.iter().map(|(n, (p, r))| json!({ "name": n, "params": format!("{p:?}"), "results": format!("{r:?}") })).collect(); - let v = json!({ "profile": wai_wasmq::profile::PROFILE, "ok": true, "blake3": wai_wasmq::envelope::blake3_hex(&bytes), "prepared_blake3": wai_wasmq::envelope::blake3_hex(&prepared), "memory_pages": f.memory, "floats": f.floats, "functions": f.defined_functions, "exports": exports }); + 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.defined_functions, f.memory, if f.floats { "floating point (canonical NaNs)" } else { "integers only" }); - println!("prepared blake3 {}", v["prepared_blake3"].as_str().unwrap_or("")); + 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:?}"); } diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index 4ae7d8106..be81fb16e 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -131,7 +131,71 @@ fn a_bundle_goes_to_no_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("no provider runs it"), "{}", text(&o)); + 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_refuses_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: an edit of it must not verify. + 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()]); + assert!(!v.status.success(), "a job whose status was edited verified:\n{}", text(&v)); +} + +#[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); +} diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index 68423e37f..f4d418d05 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -2721,7 +2721,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"), + (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-wasmq/README.md b/wai/wai-wasmq/README.md index cfe32be31..be2ed78da 100644 --- a/wai/wai-wasmq/README.md +++ b/wai/wai-wasmq/README.md @@ -7,14 +7,15 @@ An OpenQASM 3 program declares the classical functions it calls between gates wi `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 bounded, each - rule a refusal code; -- prepares it (`meter`) so that it meters its own fuel, counts its own call depth and - returns only canonical NaNs, so any WebAssembly engine runs it alike; +- holds a module to the profile (`profile`): Lime1, nothing imported, memory, size and + stack bounded, each rule a refusal code; +- 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`), runs a bundle shot by shot with each callout - metered (`run`), and seals the job receipt `wai-quantum verify` checks; +- packs and opens envelopes (`envelope`), 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 `wai-quantum verify` checks; - replays a record of calls through the modules alone (`run::replay`); - builds the reference programs (`reference`) and checks the conformance vectors (`vectors`, and the `wasmq_vectors` binary). diff --git a/wai/wai-wasmq/src/abi.rs b/wai/wai-wasmq/src/abi.rs index 9dd9e39a1..a13dee4e0 100644 --- a/wai/wai-wasmq/src/abi.rs +++ b/wai/wai-wasmq/src/abi.rs @@ -120,7 +120,7 @@ fn charge(inst: &mut dyn Instance, n: u64) -> Result<(), Stop> { } fn guest(inst: &mut dyn Instance, name: &str, args: &[u64]) -> Result, Stop> { - inst.set_depth(0); + inst.set_stack(0); Ok(inst.call(name, args)?) } diff --git a/wai/wai-wasmq/src/engine.rs b/wai/wai-wasmq/src/engine.rs index 6000ee40e..83242ebb8 100644 --- a/wai/wai-wasmq/src/engine.rs +++ b/wai/wai-wasmq/src/engine.rs @@ -3,7 +3,7 @@ //! runs the original. Both give the same results, the same fuel and the same fault. use crate::interp; -use crate::profile::MAX_CALL_DEPTH; +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)] @@ -18,7 +18,7 @@ pub enum Trap { InvalidConversion, /// The call's fuel ran out (§4.2). Fuel, - /// More calls in progress than the profile allows (§4.3). + /// 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), @@ -37,7 +37,7 @@ impl Trap { Trap::IntegerOverflow => "integer_overflow", Trap::InvalidConversion => "invalid_conversion_to_integer", Trap::Fuel => "fuel_exhausted", - Trap::Stack => "call_depth_exhausted", + Trap::Stack => "stack_exhausted", Trap::Host(_) => "host", } } @@ -49,10 +49,13 @@ pub trait Instance { fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap>; fn fuel(&mut self) -> i64; fn set_fuel(&mut self, fuel: i64); - fn set_depth(&mut self, depth: u32); - /// `len` bytes of memory from `at`, if they are inside it. + /// 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`, if it fits inside the memory. + /// 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; } @@ -69,12 +72,12 @@ pub fn available() -> Vec<&'static str> { [native, &["interpreter", "interpreter-prepared"]].concat() } -/// A trap a prepared module raised, told apart by its globals: its own fuel or depth +/// 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, depth: u32) -> Trap { +pub(crate) fn refine(t: Trap, fuel: i64, stack: u32) -> Trap { match t { Trap::Unreachable if fuel < 0 => Trap::Fuel, - Trap::Unreachable if depth > MAX_CALL_DEPTH => Trap::Stack, + Trap::Unreachable if stack > MAX_STACK => Trap::Stack, t => t, } } @@ -102,7 +105,7 @@ struct InterpInstance<'a> { 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.depth()) } else { t }) + 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() @@ -110,14 +113,14 @@ impl Instance for InterpInstance<'_> { fn set_fuel(&mut self, fuel: i64) { self.i.set_fuel(fuel) } - fn set_depth(&mut self, depth: u32) { - self.i.set_depth(depth) + 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) + 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().get_mut(at as usize..(at as usize).saturating_add(data.len())) { + 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 @@ -153,9 +156,18 @@ pub use native::{Native, native_fuel}; #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] mod native { use super::*; - use crate::meter; + 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); + /// 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; @@ -186,8 +198,8 @@ mod native { .wasm_multi_value(true) .wasm_extended_const(true) .consume_fuel(true) - .max_wasm_stack(128 << 20) - .async_stack_size(129 << 20); + .max_wasm_stack(WASM_STACK) + .async_stack_size(WASM_STACK + (1 << 20)); c } @@ -205,9 +217,9 @@ mod native { Ok(Native { engine, module }) } - /// [`Native::new`] with the engine's own stack limit at `bytes` instead of the - /// profile's: only the test that shows the profile's deepest frames are real, and - /// fit the limit `new` sets, uses it. + /// [`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)); @@ -246,7 +258,7 @@ mod native { store: Store, instance: wasmtime::Instance, fuel: wasmtime::Global, - depth: wasmtime::Global, + stack: wasmtime::Global, memory: Option, } @@ -260,14 +272,16 @@ mod native { 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 depth = instance.get_global(&mut store, meter::DEPTH).ok_or("the module is not prepared: no depth global")?; - let memory = instance.exports(&mut store).find_map(|e| e.into_memory()); - Ok(Box::new(NativeInstance { store, instance, fuel, depth, memory })) + 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 Instance for NativeInstance { - fn call(&mut self, name: &str, args: &[u64]) -> Result, Trap> { + 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 @@ -313,19 +327,32 @@ mod native { Some(other) => Trap::Host(format!("the engine stopped the call: {other}")), None => Trap::Host(format!("the engine failed: {e}")), }; - let (fuel, depth) = (self.fuel(), self.depth.get(&mut self.store).i32().unwrap_or(0) as u32); - Err(refine(t, fuel, depth)) + 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> { + 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_depth(&mut self, depth: u32) { - let _ = self.depth.set(&mut self.store, Val::I32(depth as i32)); + 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?; diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index 3bc4f5ac5..30cd42126 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -21,10 +21,13 @@ pub const CAPABILITY: &str = "wai.quantum.wasmq"; pub const MAGIC: &[u8; 4] = b"WQM1"; /// The most shots a run takes. pub const MAX_SHOTS: u64 = 1 << 20; -/// The largest number a budget, a seed or a count may be: every JSON reader holds it exactly. +/// The largest number a budget, a seed, an input or a count may be: every JSON reader +/// holds it exactly. pub const MAX_EXACT: u64 = (1 << 53) - 1; -/// The most qubits the state vector takes in a run; the stabilizer method takes any. -pub const MAX_DENSE_QUBITS: u32 = 24; +/// The most qubits a program may declare: what the method's tableau holds. +pub const MAX_QUBITS: u32 = 1 << 15; +/// The one method of this version of the profile (§2.2). +pub const METHOD: &str = "stabilizer"; pub fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() @@ -94,25 +97,26 @@ 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. +/// 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::*; let h = wai_quantum::qasm::read_callouts(program, inputs).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")); } - let method = if p.clifford_ops().is_ok() { - "stabilizer" - } else if p.n_qubits <= MAX_DENSE_QUBITS { - "statevector" - } else { - return Err(refuse("W016", format!("{} qubits with gates outside the Clifford group: the runner holds a state vector of at most {MAX_DENSE_QUBITS}", p.n_qubits))); - }; - Ok((h, method.to_string())) + 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))); + } + if p.n_qubits > MAX_QUBITS { + return Err(refuse("W016", format!("{} qubits; the method holds at most {MAX_QUBITS}", p.n_qubits))); + } + Ok((h, METHOD.to_string())) } -/// Checks a bundle as [`pack`] and [`open`] do: the program, the modules, the callouts -/// against the modules' exports, and the run. The method it fixes. +/// 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 (p, method) = read(&b.program, &b.inputs)?; let r = &b.run; @@ -124,49 +128,63 @@ pub fn check(b: &Bundle) -> Result { return Err(refuse("W017", format!("the {what}, {v}, is past 2^53 - 1: a receipt could not bind it exactly"))); } } + 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 {}, but the program runs by {method}", r.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 { - facts.push(profile::check(&m.bytes).map_err(|e| refuse(e.code, format!("module {}: {}", m.name, e.message)))?); + 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)?; } - for d in &p.externs { - let c = b.callouts.iter().find(|c| c.name == d.name).ok_or_else(|| refuse("W015", format!("the program declares `extern {}`, and no module is named for it", d.name)))?; + // 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 {} exports no function {}", b.modules[c.module].name, 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 {} exports {} as {:?} -> {:?}", d.name, want.0, want.1, b.modules[c.module].name, c.export, got.0, got.1))); + 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::takes_list(d) && f.functions.get("cabi_realloc").map(|t| t.0.len() == 4 && t.1.len() == 1) != Some(true) { - return Err(refuse("W015", format!("{} passes a list, and module {} exports no cabi_realloc(i32, i32, i32, i32) -> i32", d.name, b.modules[c.module].name))); - } - if abi::uses_memory(d) && f.memory.is_none() { - return Err(refuse("W015", format!("{} passes a list, and module {} has no memory", d.name, b.modules[c.module].name))); + if abi::uses_memory(d) { + reachable(f, module, &format!("{} passes a list", d.name), abi::takes_list(d))?; } } - for c in &b.callouts { - if !p.externs.iter().any(|d| d.name == c.name) { - return Err(refuse("W015", format!("a callout {} that the program does not declare", c.name))); + 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))); } - } - for (m, f) in b.modules.iter().zip(&facts) { - if !m.priors.is_empty() && f.functions.get("wasmq-prior").map(|t| t.0.len() == 3 && t.1.is_empty()) != Some(true) { - return Err(refuse("W015", format!("module {} has priors and exports no wasmq-prior(i32, i32, i32)", m.name))); - } - for (name, want) in [("wasmq-seed", (1usize, 0usize)), ("init", (0, 0))] { - if let Some(t) = f.functions.get(name) - && (t.0.len(), t.1.len()) != want - { - return Err(refuse("W012", format!("module {} exports {name} with the wrong type", 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) @@ -221,30 +239,66 @@ fn u32le(out: &mut Vec, v: usize) -> Result<(), Refusal> { Ok(()) } -/// Writes the envelope of `b`, after [`check`]ing it. +/// 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 contract = canonical(&contract(b, &method)); - let mut payload = MAGIC.to_vec(); - u32le(&mut payload, contract.len())?; - payload.extend_from_slice(&contract); - payload.extend_from_slice(b.program.as_bytes()); + 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 { - payload.extend_from_slice(&m.bytes); + frames.extend_from_slice(&m.bytes); for (_, prior) in &m.priors { - payload.extend_from_slice(prior); + frames.extend_from_slice(prior); } } - let manifest = manifest(b, h.reading.program.n_qubits)?; + 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); + 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]; diff --git a/wai/wai-wasmq/src/interp.rs b/wai/wai-wasmq/src/interp.rs index fc452a926..dc6cee5f9 100644 --- a/wai/wai-wasmq/src/interp.rs +++ b/wai/wai-wasmq/src/interp.rs @@ -14,16 +14,16 @@ use crate::engine::Trap; use crate::meter::{self, ends_run, weight}; -use crate::profile::{self, MAX_CALL_DEPTH}; +use crate::profile::{self, MAX_STACK}; use std::collections::BTreeMap; -use wasmparser::{BlockType, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, Payload, ValType, Validator}; +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 depth. + /// The module: a prepared module, whose globals hold the fuel and the stack's weight. Module, } @@ -44,6 +44,8 @@ enum I<'a> { struct Func<'a> { ty: u32, + /// The weight of its frame (§4.3). + weight: u32, params: u32, locals: u32, results: u32, @@ -60,6 +62,8 @@ pub struct Module<'a> { 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, } @@ -114,12 +118,13 @@ impl<'a> Module<'a> { 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 Parser::new(0).parse_all(bytes) { + for payload in profile::parser().parse_all(bytes) { let payload = payload.map_err(err)?; let valid = v.payload(&payload).map_err(err)?; match payload { @@ -165,6 +170,7 @@ impl<'a> Module<'a> { ExternalKind::Global => { m.global_exports.insert(e.name.to_string(), e.index); } + ExternalKind::Memory if e.name == profile::MEMORY => m.memory_exported = true, _ => {} } } @@ -315,8 +321,10 @@ impl<'a> Module<'a> { other => code.push(I::Op(other)), } } + ops.finish().map_err(err)?; allocs = fv.into_allocations(); - m.funcs.push(Func { ty, params, locals, results, code }); + let weight = profile::frame_weight(params, results, locals, body.range().end - body.range().start); + m.funcs.push(Func { ty, weight, params, locals, results, code }); } } Ok(m) @@ -457,17 +465,18 @@ impl<'a> Instance<'a> { } } - pub fn depth(&self) -> u32 { + /// 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::DEPTH).unwrap_or(0) as u32, + Metering::Module => self.global(meter::STACK).unwrap_or(0) as u32, } } - pub fn set_depth(&mut self, d: u32) { + pub fn set_stack_weight(&mut self, w: u32) { match self.m.metering { - Metering::Own => self.depth = d, - Metering::Module => self.set_global(meter::DEPTH, d as u64), + Metering::Own => self.depth = w, + Metering::Module => self.set_global(meter::STACK, w as u64), } } @@ -478,18 +487,20 @@ impl<'a> Instance<'a> { 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::DEPTH => v as u32 as u64, + meter::STACK => v as u32 as u64, _ => v, }; } } - pub fn memory(&self) -> &[u8] { - &self.mem + /// 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) -> &mut [u8] { - &mut 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). @@ -500,7 +511,10 @@ impl<'a> Instance<'a> { return Err(Trap::Host(format!("{name} takes {} arguments", func.params))); } self.stack.clear(); - self.stack.extend_from_slice(args); + // 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() { @@ -539,7 +553,8 @@ impl<'a> Instance<'a> { 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; - let ins = &f.code[fr.pc]; + // 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) => { @@ -588,15 +603,22 @@ impl<'a> Instance<'a> { if frames.is_empty() { return Ok(std::mem::take(&mut self.stack)); } - if m.metering == Metering::Own { - self.depth -= 1; + // 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 = *g; - self.enter(&mut frames, 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), @@ -606,25 +628,29 @@ impl<'a> Instance<'a> { 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)?; + self.enter(&mut frames, g); } I::Trap => return Err(Trap::Unreachable), } } } - fn enter(&mut self, frames: &mut Vec, g: u32) -> Result<(), Trap> { + /// 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 += 1; - if self.depth > MAX_CALL_DEPTH { + 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 }); - Ok(()) } fn addr(&mut self, offset: u64, size: usize) -> Result { diff --git a/wai/wai-wasmq/src/lib.rs b/wai/wai-wasmq/src/lib.rs index 36a64af69..734fe99d4 100644 --- a/wai/wai-wasmq/src/lib.rs +++ b/wai/wai-wasmq/src/lib.rs @@ -5,13 +5,15 @@ //! 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 bounded. It is prepared ([`meter`]) so that it meters its own fuel, -//! bounds its own call depth and returns only canonical NaNs, so that every engine runs -//! it with the same results and the same fuel. Two engines run it here ([`engine`]): the -//! pinned native engine and an interpreter of this crate's own ([`interp`]), the only one -//! on wasm32. A run ([`run`]) goes shot by shot, 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. +//! 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; diff --git a/wai/wai-wasmq/src/meter.rs b/wai/wai-wasmq/src/meter.rs index b3550fe05..5b33842d8 100644 --- a/wai/wai-wasmq/src/meter.rs +++ b/wai/wai-wasmq/src/meter.rs @@ -1,5 +1,5 @@ -//! The prepared module (extensions/quantum-wasmq.md §4): the module rewritten so that it -//! meters its own fuel, counts its own calls and returns only canonical NaNs, whatever +//! 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 @@ -7,23 +7,28 @@ //! //! - 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`, `memory.fill` and `memory.init`, a charge of the byte count, -//! and before `memory.grow`, a charge of the page count; -//! - around each `call` and `call_indirect`, a count of the calls in progress in the -//! global `__wasmq_depth`, trapping past [`MAX_CALL_DEPTH`]; +//! - before `memory.copy` and `memory.fill`, a charge of the byte count, and before +//! `memory.grow`, a charge of the page count; +//! - 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 depth after a call, and tells a fuel trap (`__wasmq_fuel < 0`) and a depth -//! trap (`__wasmq_depth > MAX_CALL_DEPTH`) from the module's own. +//! 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_CALL_DEPTH, Refusal}; -use wasmparser::{Operator, Parser, Payload}; +use crate::profile::{self, MAX_STACK, Refusal}; +use wasmparser::{Operator, Payload}; /// The exported fuel global. pub const FUEL: &str = "__wasmq_fuel"; -/// The exported call-depth global. -pub const DEPTH: &str = "__wasmq_depth"; +/// 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 @@ -46,11 +51,11 @@ pub fn ends_run(op: &Operator<'_>) -> bool { matches!(op, Block { .. } | Loop { .. } | If { .. } | Else | End | Br { .. } | BrIf { .. } | BrTable { .. } | Return | Call { .. } | CallIndirect { .. } | Unreachable) } -/// The instruction's variable charge (§4.2), if it has one: `memory.copy`, `memory.fill` -/// and `memory.init` are charged one for each byte, `memory.grow` one for each page. +/// Whether the instruction has a variable charge (§4.2): `memory.copy` and `memory.fill` +/// are charged one for each byte, `memory.grow` one for each page. pub fn variable(op: &Operator<'_>) -> bool { use Operator::*; - matches!(op, MemoryCopy { .. } | MemoryFill { .. } | MemoryInit { .. } | MemoryGrow { .. }) + matches!(op, MemoryCopy { .. } | MemoryFill { .. } | MemoryGrow { .. }) } /// Whether the instruction's result is canonicalised (§4.5): an arithmetic instruction @@ -68,12 +73,12 @@ pub fn nan_result(op: &Operator<'_>) -> Option { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Parts { pub fuel: bool, - pub depth: bool, + pub stack: bool, pub nan: bool, } impl Parts { - pub const ALL: Parts = Parts { fuel: true, depth: true, nan: true }; + pub const ALL: Parts = Parts { fuel: true, stack: true, nan: true }; } pub(crate) fn leb_u(out: &mut Vec, mut v: u64) { @@ -133,7 +138,7 @@ fn appended(raw: Option<&[u8]>, extra: u64, more: &[u8]) -> Vec { struct Globals { fuel: u32, - depth: u32, + stack: u32, } fn charge_const(out: &mut Vec, g: &Globals, cost: u64) { @@ -167,23 +172,30 @@ fn charge_top(out: &mut Vec, g: &Globals, t: u32) { fuel_check(out, g); } -fn depth_in(out: &mut Vec, g: &Globals) { +/// 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.depth as u64); - out.extend_from_slice(&[0x41, 0x01, 0x6a, 0x24]); - leb_u(out, g.depth as u64); - out.push(0x23); - leb_u(out, g.depth as u64); + leb_u(out, g.stack as u64); out.push(0x41); - leb_s(out, MAX_CALL_DEPTH as i64); + 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]); } -fn depth_out(out: &mut Vec, g: &Globals) { +/// 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.depth as u64); - out.extend_from_slice(&[0x41, 0x01, 0x6b, 0x24]); - leb_u(out, g.depth as u64); + 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. @@ -204,8 +216,10 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { let mut types: Vec = Vec::new(); // parameter count of each type let mut func_types: Vec = Vec::new(); let mut n_globals = 0u32; - let mut bodies: Vec> = Vec::new(); - for payload in Parser::new(0).parse_all(bytes) { + // 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() @@ -225,38 +239,42 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { } } Payload::GlobalSection(r) => n_globals = r.count(), - Payload::CodeSectionEntry(body) => bodies.push(body), + 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, depth: n_globals + 1 }; + let g = Globals { fuel: n_globals, stack: n_globals + 1 }; let n_funcs = func_types.len() as u32; - let helpers = parts.nan && facts.floats; + // 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, body) in bodies.iter().enumerate() { + for (k, (whole, ops_at, declared, list)) in bodies.iter().enumerate() { let params = types[func_types[k] as usize]; - 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 decls = &whole[..ops_at]; - 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)); - } + 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 as u32; + let t = params + declared; let mut f = Vec::new(); if needs_t { let (n, at) = read_leb(decls); @@ -287,12 +305,12 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { charge_top(&mut f, &g, t); } let call = matches!(op, Operator::Call { .. } | Operator::CallIndirect { .. }); - if parts.depth && call { - depth_in(&mut f, &g); + if parts.stack && call { + stack_in(&mut f, &g, weight_here); } f.extend_from_slice(&whole[*a..*b]); - if parts.depth && call { - depth_out(&mut f, &g); + if parts.stack && call { + stack_out(&mut f, &g, weight_here); } if helpers && let Some(wide) = nan_result(op) { f.push(0x10); @@ -325,7 +343,7 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { 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), (DEPTH, g.depth)] { + 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); @@ -342,12 +360,11 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { } } } - if let Ok(dump) = std::env::var("WASMQ_DUMP_PREPARED") { - let _ = std::fs::write(dump, &out); - } - // The prepared module is itself a module in the feature set. + // 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("W001", format!("the prepared module does not validate: {}", e.message())))?; + 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) } @@ -370,6 +387,61 @@ mod tests { assert_eq!(b, [0xc0, 0x00]); } + /// The largest growth preparing can cause: a body at the profile's limit that is all + /// calls, with as many globals, functions and types as the profile takes, so that + /// every added index is as long as it gets, and a float so the two functions are added. + #[test] + fn a_module_at_every_limit_prepares_within_an_engine_s_limits() { + use crate::profile::{MAX_BODY_BYTES, MAX_ITEMS}; + 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 + }; + let n = MAX_ITEMS as u64; + // MAX_ITEMS - 2 types `() -> ()` (preparing adds two), and as many functions. + let mut types = leb(n - 2); + for _ in 0..n - 2 { + types.extend_from_slice(&[0x60, 0x00, 0x00]); + } + let mut funcs = leb(n - 2); + funcs.extend(std::iter::repeat_n(0u8, (n - 2) as usize)); + // MAX_ITEMS - 2 globals: the two added ones then have the longest indices. + let mut globals = leb(n - 2); + for _ in 0..n - 2 { + 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); + assert!(big.len() as u64 >= MAX_BODY_BYTES - 1); + let mut code = leb(n - 2); + code.extend(leb(big.len() as u64)); + code.extend_from_slice(&big); + for _ in 0..n - 3 { + 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 by about thirty times, and is still under the 7,654,321 bytes an + // engine takes for one function. + let growth = p.len() as f64 / m.len() as f64; + assert!(p.len() > 3_000_000 && p.len() < 7_000_000, "{} bytes, {growth:.1} times", p.len()); + } + #[test] fn preparing_is_a_function_of_the_bytes_and_keeps_every_instruction() { let m = wat::parse_str( diff --git a/wai/wai-wasmq/src/profile.rs b/wai/wai-wasmq/src/profile.rs index 6c879e1ef..d6bfe246a 100644 --- a/wai/wai-wasmq/src/profile.rs +++ b/wai/wai-wasmq/src/profile.rs @@ -3,11 +3,16 @@ //! 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 the first rule it breaks ([`Refusal`]), each rule with a stable code. +//! ([`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::{ - ConstExpr, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, Payload, TypeRef, ValType, Validator, + CompositeInnerType, ConstExpr, DataKind, ElementItems, ElementKind, ExternalKind, FuncType, Operator, Parser, Payload, TypeRef, ValType, Validator, WasmFeatures, }; @@ -15,18 +20,31 @@ use wasmparser::{ pub const PROFILE: &str = "wai.quantum.wasmq/1"; /// 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. -pub const MAX_MODULE_BYTES: usize = 16 << 20; -/// The most functions a module may define. -pub const MAX_FUNCTIONS: u32 = 1 << 20; +/// The most bytes a module may have (4 MiB). +pub const MAX_MODULE_BYTES: usize = 4 << 20; +/// 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 (128 KiB). +pub const MAX_BODY_BYTES: u64 = 1 << 17; /// The most locals and operands one function's frame may hold at once. pub const MAX_FRAME_SLOTS: u32 = 4096; -/// The most calls that may be in progress at once inside one host call (§4.3). -pub const MAX_CALL_DEPTH: u32 = 1024; +/// 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)] @@ -49,23 +67,23 @@ impl std::fmt::Display for Refusal { /// 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 (a component, a malformed module, or an instruction no rule below names)"), + ("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, shared, 64-bit, without a maximum, or a maximum past the profile's"), + ("W003", "the memory: more than one, 64-bit, a page size of its own, without a maximum, or a maximum past the profile's"), ("W004", "a start function: nothing runs when a module is instantiated"), - ("W005", "threads: shared memory or an atomic instruction"), - ("W006", "a vector (SIMD) instruction, relaxed or not"), - ("W007", "a reference, exception, tail-call, garbage-collected or other instruction or type outside Lime1's numbers"), - ("W008", "the table: more than one, a type other than funcref, an instruction that changes it, or larger than the profile allows"), + ("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", "the module, or its count of functions, is larger than the profile allows"), + ("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 the profile names with the wrong type"), - ("W013", "the envelope: not a WAI1 envelope of this capability, a contract that is not canonical, or frames that do not add up"), + ("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, 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", "a callout whose module does not export it with the signature its `extern` lowers to (§5.2)"), - ("W016", "the program: the reader refuses it, it calls nothing, or its method or size is not one the runner takes"), - ("W017", "the run: shots, seed or a fuel budget outside what the profile and a receipt can hold"), + ("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, or more qubits than the method holds"), + ("W017", "the run: shots, seed, an input or a fuel budget outside what the profile and a receipt can hold"), ]; /// What the rest of the crate needs to know about a module that passes. @@ -73,16 +91,14 @@ pub const CODES: &[(&str, &str)] = &[ pub struct Facts { /// Each exported function by name: its parameter and result types. pub functions: BTreeMap, Vec)>, - /// The function index of each exported function. - pub function_index: BTreeMap, /// The memory's initial and maximum pages, if it has one. pub memory: Option<(u64, u64)>, - /// The name the memory is exported under, if it is. - pub memory_export: Option, - /// Whether any function uses a floating-point instruction. + /// 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, - /// Functions the module defines. - pub defined_functions: u32, + /// The frame weight of each function, by its index (§4.3). + pub weights: Vec, } fn has_float(ty: &ValType) -> bool { @@ -96,59 +112,281 @@ pub fn features() -> WasmFeatures { WasmFeatures::LIME1 } -/// Reads `bytes` as a wasmQ module: its facts, or the first rule it breaks. +/// 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 and each byte of its entry in the code section. +pub fn frame_weight(params: u32, results: u32, locals: u32, body_bytes: u64) -> u32 { + FRAME_BASE + params + results + locals + 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()))); } - // The shape first, from a permissive reading, so that a module outside the profile is - // refused with the rule it breaks rather than the validator's first complaint. - shape(bytes)?; + scan(bytes)?; let mut v = Validator::new_with_features(features()); - let mut facts = Facts { - functions: BTreeMap::new(), - function_index: BTreeMap::new(), - memory: None, - memory_export: None, - floats: false, - defined_functions: 0, - }; + 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::new(0).parse_all(bytes) { - let payload = payload.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - match v.payload(&payload).map_err(|e| invalid(bytes, e))? { + 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 mut locals = 0u64; - let mut lr = body.get_locals_reader().map_err(|e| Refusal::new("W001", e.message().to_string()))?; let idx = fv.index(); - let params = types[func_types[idx as usize] as usize].params().len() as u64; + 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(|e| Refusal::new("W001", e.message().to_string()))?; + 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 {} locals; a frame holds at most {MAX_FRAME_SLOTS} locals and operands", params + locals))); + 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(|e| invalid(bytes, e))?; + fv.define_locals(off, n, ty).map_err(malformed)?; } - let mut ops = body.get_operators_reader().map_err(|e| Refusal::new("W001", e.message().to_string()))?; + let mut ops = body.get_operators_reader().map_err(malformed)?; while !ops.eof() { - let (op, off) = ops.read_with_offset().map_err(|e| Refusal::new("W001", e.message().to_string()))?; - rule(&op, off as usize)?; + let (op, off) = ops.read_with_offset().map_err(malformed)?; facts.floats |= float_op(&op); - fv.op(off, &op).map_err(|e| invalid(bytes, e))?; + fv.op(off, &op).map_err(malformed)?; 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, size)); } wasmparser::ValidPayload::End(_) => {} _ => {} @@ -156,39 +394,30 @@ pub fn check(bytes: &[u8]) -> Result { match payload { Payload::TypeSection(r) => { for rec in r.into_iter_err_on_gc_types() { - let t = rec.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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(|e| Refusal::new("W001", e.message().to_string()))?); - } - facts.defined_functions = func_types.len() as u32; - if facts.defined_functions > MAX_FUNCTIONS { - return Err(Refusal::new("W010", format!("{} functions; the profile takes at most {MAX_FUNCTIONS}", facts.defined_functions))); + func_types.push(t.map_err(malformed)?); } } Payload::TableSection(r) => { for t in r { - let t = t.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - let max = t.ty.maximum.unwrap_or(t.ty.initial); - if max > MAX_TABLE || t.ty.initial > MAX_TABLE { - return Err(Refusal::new("W008", format!("a table of up to {max} entries; the profile takes at most {MAX_TABLE}"))); - } - table_min = Some(t.ty.initial); + table_min = Some(t.map_err(malformed)?.ty.initial); } } Payload::MemorySection(r) => { for m in r { - let m = m.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - facts.memory = Some((m.initial, m.maximum.unwrap_or(0))); + 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(|e| Refusal::new("W001", e.message().to_string()))?; + 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 })); @@ -196,17 +425,22 @@ pub fn check(bytes: &[u8]) -> Result { } Payload::ExportSection(r) => { for e in r { - let e = e.map_err(|e| Refusal::new("W001", e.message().to_string()))?; + 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 => { - let t = &types[func_types[e.index as usize] as usize]; - facts.functions.insert(e.name.to_string(), (t.params().to_vec(), t.results().to_vec())); - facts.function_index.insert(e.name.to_string(), e.index); + 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 => facts.memory_export = Some(e.name.to_string()), + ExternalKind::Memory if e.name == MEMORY => facts.memory_exported = true, _ => {} } } @@ -214,19 +448,22 @@ pub fn check(bytes: &[u8]) -> Result { Payload::DataSection(r) => { let size = facts.memory.map(|(min, _)| min << 16).unwrap_or(0); for d in r { - let d = d.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - if let DataKind::Active { offset_expr, .. } = d.kind { - 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"))); + 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(|e| Refusal::new("W001", e.message().to_string()))?; + 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")), @@ -299,131 +536,6 @@ pub fn float_op(op: &Operator<'_>) -> bool { ) } -/// The rules Lime1's validator does not hold: the table is never changed and never read -/// as a value (§3.2). -fn rule(op: &Operator<'_>, off: usize) -> Result<(), Refusal> { - use Operator::*; - match op { - TableGet { .. } | TableSet { .. } | TableGrow { .. } | TableSize { .. } | TableFill { .. } | TableCopy { .. } | TableInit { .. } | ElemDrop { .. } => { - Err(Refusal::new("W008", format!("a table instruction at offset {off}: the table is filled once and only `call_indirect` reads it"))) - } - RefNull { .. } | RefFunc { .. } | RefIsNull => Err(Refusal::new("W007", format!("a reference instruction at offset {off}"))), - _ => Ok(()), - } -} - -/// The validator's complaint, with the rule it most likely names. -fn invalid(bytes: &[u8], e: wasmparser::BinaryReaderError) -> Refusal { - let first = classify(bytes); - Refusal::new(first.0, format!("{}{}", first.1, e.message())) -} - -/// What a module outside the profile reaches for, read permissively: the first -/// instruction or construct, with the code it is refused under. -fn classify(bytes: &[u8]) -> (&'static str, String) { - let mut v = Validator::new_with_features(WasmFeatures::all()); - for payload in Parser::new(0).parse_all(bytes) { - let Ok(payload) = payload else { break }; - if let Ok(wasmparser::ValidPayload::Func(_, body)) = v.payload(&payload) { - let Ok(mut ops) = body.get_operators_reader() else { break }; - while !ops.eof() { - let Ok((op, off)) = ops.read_with_offset() else { break }; - let name = format!("{op:?}"); - let head = name.split(|c: char| !c.is_ascii_alphanumeric()).next().unwrap_or(""); - if head.contains("Atomic") || head.starts_with("MemoryAtomic") { - return ("W005", format!("{head} at offset {off}: ")); - } - if (head.starts_with("Return") && head != "Return") || ["Try", "Throw", "Rethrow", "Delegate", "Struct", "Array", "Ref", "BrOn", "Any", "Extern", "I31", "CallRef"].iter().any(|p| head.starts_with(p)) { - return ("W007", format!("{head} at offset {off}: ")); - } - if head.starts_with("V128") || head.starts_with("I8x16") || head.starts_with("I16x8") || head.starts_with("I32x4") || head.starts_with("I64x2") || head.starts_with("F32x4") || head.starts_with("F64x2") { - return ("W006", format!("{head} at offset {off}: ")); - } - } - } - } - ("W001", String::new()) -} - -/// The module's shape: the rules a permissive reading can decide before validation. -fn shape(bytes: &[u8]) -> Result<(), Refusal> { - let mut memories = 0; - for payload in Parser::new(0).parse_all(bytes) { - let payload = payload.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - match payload { - 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::ImportSection(r) => { - if let Some(i) = r.into_imports().next() { - let i = i.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - 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::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(|e| Refusal::new("W001", e.message().to_string()))?; - 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 custom page size")); - } - 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) => { - let mut n = 0; - for t in r { - let t = t.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - n += 1; - if n > 1 { - return Err(Refusal::new("W008", "more than one table")); - } - if t.ty.element_type != wasmparser::RefType::FUNCREF || t.ty.table64 || t.ty.shared { - return Err(Refusal::new("W008", "a table that is not a 32-bit, unshared table of funcref")); - } - } - } - Payload::TagSection(_) => return Err(Refusal::new("W007", "a tag (exception handling)")), - Payload::GlobalSection(r) => { - for g in r { - let g = g.map_err(|e| Refusal::new("W001", e.message().to_string()))?; - if g.ty.shared { - return Err(Refusal::new("W005", "a shared global")); - } - if !matches!(g.ty.content_type, ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64) { - return Err(Refusal::new(if g.ty.content_type == ValType::V128 { "W006" } else { "W007" }, "a global that is not a number")); - } - } - } - _ => {} - } - } - Ok(()) -} - #[cfg(test)] mod tests { use super::*; @@ -448,9 +560,12 @@ mod tests { 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"); @@ -463,11 +578,39 @@ mod tests { 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 floats_are_noticed() { + 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). + 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 + 6]); } } diff --git a/wai/wai-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs index a0c14e9b0..d63841443 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -1,7 +1,9 @@ -//! Running a bundle (extensions/quantum-wasmq.md §6): the program's steps shot by shot, -//! 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. +//! 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}; @@ -12,13 +14,30 @@ use crate::profile; use serde_json::{Map, Value, json}; use std::collections::BTreeMap; use wai_quantum::ed25519_dalek::SigningKey; -use wai_quantum::engine::dynamic::{Dense, Engine as QState, Stabilizer}; use wai_quantum::qasm::{CalloutBit, Instr, Program, Step}; +use wai_quantum::quantum_tableau::{Clifford, Tableau}; /// The result's format. pub const FORMAT: &str = "wai.quantum.wasmq.result/1"; -/// What a receipt's `energy` says: no meter reads a run here. +/// What a result's `energy` says (§6.4): no meter reads a run. pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; +/// The members of a job body (§6.4). Only `request` and `result` are under the signature; +/// `status` and `method` repeat what the result says, and [`verify`] holds them to it. +pub const BODY: [&str; 4] = ["status", "method", "request", "result"]; +/// The fault reasons a receipt may name (§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 engine a run uses. None of them appears in a receipt: they give the same bytes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -81,19 +100,22 @@ pub fn shot_seed(seed: u64, shot: u64) -> u64 { z ^ (z >> 31) } -struct Rng(u64); +/// A shot's stream of outcomes (§6.1): SplitMix64 from the shot's seed XOR `0x51574d31`. +pub struct Outcomes(u64); -impl Rng { - fn next(&mut self) -> 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) - } - /// Whether a measurement whose outcome is 1 with probability `p` reads 1. - fn reads_one(&mut self, p: f64) -> bool { - ((self.next() >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0 < p + (z ^ (z >> 31)) < (1 << 63) } } @@ -103,44 +125,78 @@ struct Budget { } impl Budget { - /// The fuel a call may use: the call's budget, or what is left of the run's. - fn limit(&self, per: u64) -> (u64, &'static str) { - if per <= self.job_left { (per, "call") } else { (self.job_left, "run") } + /// 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) } } } -struct Tally { - used: u64, - init: u64, - calls: u64, - functions: BTreeMap, - transcript: blake3::Hasher, - log: Option>, -} - -/// A run's fault, before it is written into the result. +/// A call that stopped, before it is written into the result. struct Stopped { fault: Fault, - shot: u64, phase: &'static str, - callout: Option, - module: String, - scope: &'static str, + /// Whose budget ran out, for a fuel fault. + budget: &'static str, fuel: u64, } -/// A lifecycle call of an instance (§5.3), to be metered. -type Lifecycle = Box Result<(), Stop>>; - -/// One metered stretch of guest work: set the fuel, run `f`, read what was used. -fn metered(inst: &mut dyn Instance, b: &mut Budget, per: u64, f: impl FnOnce(&mut dyn Instance) -> Result) -> (Result, u64, &'static str) { - let (limit, scope) = b.limit(per); +/// 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, scope) + (r, used, if run { "run" } else { own }) +} + +/// 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 (m, p) in b.modules.iter().zip(prepared) { + 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 @@ -148,30 +204,8 @@ fn metered(inst: &mut dyn Instance, b: &mut Budget, per: u64, f: impl FnOnce( pub fn run(opened: &Opened, kind: EngineKind, key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { let b = &opened.bundle; let prepared: Vec> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; - let work = || -> Result<(Value, Option>), Error> { - let mut engines: Vec> = Vec::new(); - for (m, p) in b.modules.iter().zip(&prepared) { - engines.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)?), - }); - } - execute(opened, &prepared, &engines, key, log) - }; - // The native engine runs a call's frames on this thread's stack: give it one deep - // enough for the profile's call depth (§4.3). - #[cfg(not(target_arch = "wasm32"))] - { - std::thread::scope(|s| std::thread::Builder::new().stack_size(512 << 20).spawn_scoped(s, work).map_err(|e| Error::Host(e.to_string()))?.join().map_err(|_| Error::Host("the runner's thread panicked".into()))?) - } - #[cfg(target_arch = "wasm32")] - { - work() - } + let engines = engines(b, &prepared, kind)?; + execute(opened, &engines, key, log) } /// The width of the bits a program's steps read and write. @@ -195,95 +229,61 @@ fn width(p: &Program, steps: &[Step]) -> usize { w } -fn execute<'e>(opened: &Opened, prepared: &[Vec], engines: &'e [Box], key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { - let mut live: Vec>> = engines.iter().map(|_| None).collect(); +fn execute<'e>(opened: &Opened, engines: &'e [Box], key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { 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 t = Tally { used: 0, init: 0, calls: 0, functions: BTreeMap::new(), transcript: blake3::Hasher::new(), log: log.then(Vec::new) }; - t.transcript.update(b"wai:wasmq-transcript\x01"); + let (mut used, mut init, mut calls) = (0u64, 0u64, 0u64); + let mut by_callout: BTreeMap = BTreeMap::new(); + let mut transcript = blake3::Hasher::new(); + transcript.update(b"wai:wasmq-transcript\x01"); + let mut record = log.then(Vec::new); let mut counts: BTreeMap = BTreeMap::new(); let mut shots_run = 0u64; - let mut stopped: Option = None; + // The fault that stopped the run: where, and what. + let mut stopped: Option<(Stopped, u64, String, Option)> = None; - let gates = if method == "statevector" { p.matrices() } else { Vec::new() }; - let cliffords: Vec> = if method == "stabilizer" { - p.clifford_ops().map_err(|e| Error::Refused(profile::Refusal::new("W016", e)))?.into_iter().map(|(g, _)| g).collect() - } else { - Vec::new() - }; '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, m) in b.modules.iter().enumerate() { - let mut i = engines[k].instantiate().map_err(Error::Host)?; - let f = &facts[k]; - let seed = if b.run.state == "shot" { shot_seed(b.run.seed, shot) } else { b.run.seed }; - let mut phases: Vec<(&'static str, Lifecycle)> = Vec::new(); - if f.functions.contains_key("wasmq-seed") { - phases.push(("seed", Box::new(move |i: &mut dyn Instance| abi::lifecycle(i, "wasmq-seed", &[seed])))); - } - for (j, (_, bytes)) in m.priors.iter().enumerate() { - let bytes = bytes.clone(); - phases.push(("prior", Box::new(move |i: &mut dyn Instance| abi::prior(i, j as u32, &bytes)))); - } - if f.functions.contains_key("init") { - phases.push(("init", Box::new(|i: &mut dyn Instance| abi::lifecycle(i, "init", &[])))); - } - for (phase, f) in phases { - let (r, used, scope) = metered(i.as_mut(), &mut budget, b.run.per_init, f); - t.used += used; - t.init += used; - match r { - Ok(()) => {} - Err(Stop::Host(h)) => return Err(Error::Host(h)), - Err(Stop::Fault(fault)) => { - let scope = if fault.reason == "fuel_exhausted" { if scope == "call" { "instance" } else { "run" } } else { "" }; - stopped = Some(Stopped { fault, shot, phase, callout: None, module: m.name.clone(), scope, fuel: used }); - break 'shots; - } + for k in 0..b.modules.len() { + let mut n = 0; + 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; } } - live[k] = Some(i); } } - let mut rng = Rng(shot_seed(b.run.seed, shot) ^ 0x5157_4d31); + let mut outcomes = Outcomes::of_shot(b.run.seed, shot); let mut bits = vec![false; w]; - enum St<'a> { - D(Dense<'a>), - S(Stabilizer<'a>), - } - let mut st = match method.as_str() { - "statevector" => St::D(Dense { sv: wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).map_err(|e| Error::Host(format!("{e:?}")))?, gates: &gates }), - _ => St::S(Stabilizer { t: wai_quantum::quantum_tableau::Tableau::new(p.n_qubits).map_err(|e| Error::Host(e.to_string()))?, ops: &cliffords }), - }; - macro_rules! q { - ($s:ident => $e:expr) => { - match &mut st { - St::D($s) => $e, - St::S($s) => $e, - } - }; - } + 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)) => q!(s => s.gate(*i)).map_err(Error::Host)?, + Step::Instr(Instr::Gate(i)) => { + for &c in &gates[*i] { + t.apply(c).map_err(host)?; + } + } Step::Instr(Instr::Measure { qubit, bit }) => { - let p1 = q!(s => s.prob_one(*qubit)).map_err(Error::Host)?; - let one = rng.reads_one(p1); - q!(s => s.collapse(*qubit, one)).map_err(Error::Host)?; - bits[*bit as usize] = one; + bits[*bit as usize] = t.measure(*qubit, || outcomes.next_reads_one()).map_err(host)?.0; } Step::Instr(Instr::Reset(qubit)) => { - let p1 = q!(s => s.prob_one(*qubit)).map_err(Error::Host)?; - let one = rng.reads_one(p1); - q!(s => s.collapse(*qubit, one)).map_err(Error::Host)?; - if one { - q!(s => s.flip(*qubit)).map_err(Error::Host)?; + 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)) => { @@ -298,21 +298,24 @@ fn execute<'e>(opened: &Opened, prepared: &[Vec], engines: &'e [Box { let decl = &externs[*func as usize]; - let c = b.callouts.iter().find(|c| c.name == decl.name).ok_or_else(|| Error::Host(format!("no callout for {}", decl.name)))?; - let ins: Vec = input.iter().map(|x| match x { - CalloutBit::Bit(i) => bits[*i as usize], - CalloutBit::Const(v) => *v, - }).collect(); + 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, used, scope) = metered(i.as_mut(), &mut budget, per, |i| abi::call(i, decl, &c.export, &ins, has_post)); - t.used += used; - t.calls += 1; - let e = t.functions.entry(decl.name.clone()).or_default(); + 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 += used; - e.2 = e.2.max(used); + e.1 += n; + e.2 = e.2.max(n); let in_bytes = abi::pack(&ins); match r { Ok(outs) => { @@ -320,17 +323,16 @@ fn execute<'e>(opened: &Opened, prepared: &[Vec], engines: &'e [Box return Err(Error::Host(h)), Err(Stop::Fault(fault)) => { - let scope = if fault.reason == "fuel_exhausted" { scope } else { "" }; - stopped = Some(Stopped { fault, shot, phase: "call", callout: Some(decl.name.clone()), module: b.modules[c.module].name.clone(), scope, fuel: used }); + stopped = Some((Stopped { fault, phase: "call", budget: budget_name, fuel: n }, shot, b.modules[c.module].name.clone(), Some(decl.name.clone()))); break 'shots; } } @@ -342,23 +344,14 @@ fn execute<'e>(opened: &Opened, prepared: &[Vec], engines: &'e [Box = b - .modules - .iter() - .zip(prepared) - .zip(&facts) - .map(|((m, pr), f)| json!({ "name": m.name, "blake3": blake3_hex(&m.bytes), "prepared_blake3": blake3_hex(pr), "memory_pages": f.memory.map(|(a, b)| json!([a, b])).unwrap_or(Value::Null), "floats": f.floats })) - .collect(); let mut functions = Map::new(); - for (name, (calls, fuel, max)) in &t.functions { + for (name, (calls, fuel, max)) in &by_callout { functions.insert(name.clone(), json!({ "calls": calls, "fuel": fuel, "max": max })); } let mut result = json!({ @@ -371,34 +364,28 @@ fn execute<'e>(opened: &Opened, prepared: &[Vec], engines: &'e [Box>(), + "transcript_blake3": hex(transcript.finalize().as_bytes()), + "accuracy": { "class": "exact" }, }); - if let Some(s) = stopped { - let mut f = json!({ "reason": s.fault.reason, "shot": s.shot, "phase": s.phase, "module": s.module, "fuel": s.fuel }); - if let Some(c) = s.callout { + 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.scope.is_empty() { - f["budget"] = json!(s.scope); - } - if !s.fault.detail.is_empty() { - f["detail"] = json!(s.fault.detail); + if s.fault.reason == "fuel_exhausted" { + f["budget"] = json!(s.budget); } result["fault"] = f; } - Ok((seal(opened, result, key), t.log)) + Ok((seal(opened, result, key), record)) } /// Replays recorded calls against a bundle's modules (extensions/quantum-wasmq.md §7): each @@ -412,82 +399,55 @@ pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; let facts: Vec = b.modules.iter().map(|m| profile::check(&m.bytes)).collect::>().map_err(Error::Refused)?; - let work = || -> Result, Error> { - let mut engines: Vec> = Vec::new(); - for (m, pr) in b.modules.iter().zip(&prepared) { - engines.push(match kind { - #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] - EngineKind::Native => Box::new(engine::Native::new(pr).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(pr, interp::Metering::Module).map_err(Error::Host)?), - }); - } - let mut live: Vec)>> = engines.iter().map(|_| None).collect(); - let mut problems = Vec::new(); - let mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; - for (k, call) in calls.iter().enumerate() { - let Some(func) = externs.iter().position(|d| d.name == call.callout) else { - problems.push(format!("call {k}: the program declares no `extern {}`", call.callout)); - continue; - }; - let decl = &externs[func]; - let c = b.callouts.iter().find(|c| c.name == decl.name).ok_or_else(|| Error::Host(format!("no callout for {}", decl.name)))?; - let fresh = match &live[c.module] { - None => true, - Some((shot, _)) => b.run.state == "shot" && *shot != call.shot, - }; - if fresh { - let mut i = engines[c.module].instantiate().map_err(Error::Host)?; - let seed = if b.run.state == "shot" { shot_seed(b.run.seed, call.shot) } else { b.run.seed }; - let f = &facts[c.module]; - let m = &b.modules[c.module]; - if f.functions.contains_key("wasmq-seed") { - metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::lifecycle(i, "wasmq-seed", &[seed])).0.map_err(|e| Error::Host(format!("{e:?}")))?; + let engines = engines(b, &prepared, kind)?; + let mut live: Vec)>> = engines.iter().map(|_| None).collect(); + let mut problems = Vec::new(); + let mut budget = Budget { per_call: b.run.per_call, job_left: b.run.per_job }; + for (k, call) in calls.iter().enumerate() { + let Some(func) = externs.iter().position(|d| d.name == call.callout) else { + problems.push(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 { + 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) => { + problems.push(format!("call {k} ({} in shot {}): the module stops with {} in its {}", call.callout, call.shot, s.fault.reason, s.phase)); + break; } - for (j, (_, bytes)) in m.priors.iter().enumerate() { - metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::prior(i, j as u32, bytes)).0.map_err(|e| Error::Host(format!("{e:?}")))?; - } - if f.functions.contains_key("init") { - metered(i.as_mut(), &mut budget, b.run.per_init, |i| abi::lifecycle(i, "init", &[])).0.map_err(|e| Error::Host(format!("{e:?}")))?; - } - live[c.module] = Some((call.shot, i)); - } - let width: u32 = decl.args.iter().map(|a| a.bits()).sum(); - if call.input.len() != (width as usize).div_ceil(8) { - problems.push(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, |i| abi::call(i, decl, &c.export, &ins, has_post)); - match r { - Ok(outs) => { - let out = abi::pack(&outs); - if out != call.output { - problems.push(format!("call {k} ({} in shot {}): the module returns {}, the record says {}", call.callout, call.shot, hex(&out), hex(&call.output))); - } - if used != call.fuel { - problems.push(format!("call {k} ({} in shot {}): the module uses {used} fuel, the record says {}", call.callout, call.shot, call.fuel)); - } - } - Err(Stop::Fault(f)) => problems.push(format!("call {k} ({} in shot {}): the module stops with {}", call.callout, call.shot, f.reason)), - Err(Stop::Host(h)) => return Err(Error::Host(h)), } } - Ok(problems) - }; - #[cfg(not(target_arch = "wasm32"))] - { - std::thread::scope(|s| std::thread::Builder::new().stack_size(512 << 20).spawn_scoped(s, work).map_err(|e| Error::Host(e.to_string()))?.join().map_err(|_| Error::Host("the replay's thread panicked".into()))?) - } - #[cfg(target_arch = "wasm32")] - { - work() + let width: u32 = decl.args.iter().map(|a| a.bits()).sum(); + if call.input.len() != (width as usize).div_ceil(8) { + problems.push(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 out = abi::pack(&outs); + if out != call.output { + problems.push(format!("call {k} ({} in shot {}): the module returns {}, the record says {}", call.callout, call.shot, hex(&out), hex(&call.output))); + } + if used != call.fuel { + problems.push(format!("call {k} ({} in shot {}): the module uses {used} fuel, the record says {}", call.callout, call.shot, call.fuel)); + } + } + Err(Stop::Fault(f)) => problems.push(format!("call {k} ({} in shot {}): the module stops with {}", call.callout, call.shot, f.reason)), + Err(Stop::Host(h)) => return Err(Error::Host(h)), + } } + Ok(problems) } /// The request a receipt binds: the contract and the envelope's digest. @@ -495,13 +455,19 @@ pub fn request(opened: &Opened) -> Value { json!({ "wasmq": opened.contract, "envelope_blake3": opened.envelope_blake3 }) } +/// The status a job body carries for a result (§6.4): `done`, or `fault` for a run that +/// stopped. +pub fn body_status(result: &Value) -> &'static str { + if result["status"] == "ok" { "done" } else { "fault" } +} + /// 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 = if result["status"] == "ok" { "done" } else { "fault" }; + let status = body_status(&result); result["receipt"] = receipt; json!({ "status": status, "method": result["method"], "request": req, "result": result }) } @@ -509,7 +475,16 @@ pub fn seal(opened: &Opened, mut result: Value, key: &SigningKey) -> Value { /// 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. pub well_formed: Vec, /// With the bundle: whether it is the one the receipt binds, and whether running it /// again gives the same result, byte for byte. @@ -523,14 +498,37 @@ impl Verdict { } } -/// Checks a wasmQ job body: its signature, that its result is one this profile writes -/// (fuel within budgets, a fault reason it names), and, given the bundle, that it is the -/// bundle the receipt binds and that running it again reproduces the result. +/// Checks a wasmQ job body: its signature and who signed; that every member of the body +/// is one the signature covers or agrees with what it covers; that its result is one this +/// profile writes (fuel within budgets, a fault reason it names); and, given the bundle, +/// that it is the bundle the receipt binds and that running it again reproduces the result. pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result { - let (signature, _) = wai_quantum::engine::job::verify(job)?; - let mut v = Verdict { signature, ..Verdict::default() }; + let (signature, key) = wai_quantum::engine::job::verify(job)?; let r = &job["result"]; + let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), ..Verdict::default() }; let mut problem = |s: String| v.well_formed.push(s); + // The body: nothing in it but what the signature covers, or repeats from under it. + for (name, value) in job.as_object().into_iter().flatten() { + match name.as_str() { + "request" | "result" => {} + "status" if value == body_status(r) => {} + "status" => problem(format!("the body's status is {value}, where its signed result says {}", body_status(r))), + "method" if value == &r["method"] => {} + "method" => problem(format!("the body's method is {value}, where its signed result says {}", r["method"])), + other => problem(format!("the body holds `{other}`, which no signature covers")), + } + } + for name in BODY { + if job.get(name).is_none() { + problem(format!("the body has no `{name}`")); + } + } + // A number a double cannot hold would be read differently by readers of the receipt. + for (what, value) in [("request", &job["request"]), ("result", r)] { + if let Some(at) = wai_quantum::engine::receipt::inexact_number(value) { + problem(format!("the {what}'s `{at}` is a whole number past 2^53 - 1")); + } + } if r["format"] != FORMAT { problem(format!("the result's format is {}, not {FORMAT}", r["format"])); } @@ -556,10 +554,12 @@ pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result< if r.get("fault").is_some() { problem("a result that is ok and names a fault".into()); } + if r["shots_run"] != r["shots"] { + problem(format!("a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); + } } Some("fault") => { - let reasons = ["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", "call_depth_exhausted", "bad_result"]; - if !r["fault"]["reason"].as_str().is_some_and(|x| reasons.contains(&x)) { + if !r["fault"]["reason"].as_str().is_some_and(|x| REASONS.contains(&x)) { problem(format!("a fault the profile does not name: {}", r["fault"]["reason"])); } } diff --git a/wai/wai-wasmq/tests/adversarial.rs b/wai/wai-wasmq/tests/adversarial.rs index f09c08926..68b2d241c 100644 --- a/wai/wai-wasmq/tests/adversarial.rs +++ b/wai/wai-wasmq/tests/adversarial.rs @@ -184,11 +184,11 @@ fn an_endless_loop_stops_at_its_budget() { } #[test] -fn recursion_past_the_call_depth_ends_in_a_fault_receipt() { +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"], "call_depth_exhausted"); + assert_eq!(body["result"]["fault"]["reason"], "stack_exhausted"); } #[test] @@ -232,21 +232,61 @@ fn a_prior_whose_hash_is_not_the_pinned_one_is_refused() { 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_program_or_contract_is_refused() { +fn a_changed_module_or_program_is_refused_for_its_digest() { let env = envelope(); - let bad = tamper(&env, b"wasmq-prior", b"wasmq-PRIOR"); - assert_eq!(open(&bad).unwrap_err().code, "W014", "a module byte"); - let bad = tamper(&env, b"h q;", b"x q;"); - assert_eq!(open(&bad).unwrap_err().code, "W014", "a program byte"); - let bad = tamper(&env, b"\"shots\":4", b"\"shots\":5"); - assert_eq!(open(&bad).unwrap_err().code, "W013", "the contract's run, not the manifest's"); - let bad = tamper(&env, b"\"fallback\":null", b"\"fallback\":0 "); - assert_eq!(open(&bad).unwrap_err().code, "W013", "a manifest byte"); + 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!(open(&long).unwrap_err().code, "W013", "a byte after the payload"); - assert_eq!(open(&env[..env.len() - 1]).unwrap_err().code, "W013", "a byte short"); + 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] @@ -266,7 +306,7 @@ fn a_receipt_changed_after_sealing_or_held_to_another_bundle_does_not_verify() { } #[test] -fn without_its_meter_a_prepared_module_counts_nothing_and_without_its_depth_count_it_is_not_bounded() { +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| { @@ -274,14 +314,15 @@ fn without_its_meter_a_prepared_module_counts_nothing_and_without_its_depth_coun let e = Interpreter::new(&p, Metering::Module).unwrap(); let mut i = e.instantiate().unwrap(); i.set_fuel(1 << 40); - i.set_depth(0); + 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"); - let deep = wai_wasmq::profile::MAX_CALL_DEPTH as u64 + 10; + // 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 { depth: false, ..Parts::ALL }, deep).0.is_ok(), "this test can fail: no depth count, no bound"); + 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 index 7acca923b..0607feea8 100644 --- a/wai/wai-wasmq/tests/engines.rs +++ b/wai/wai-wasmq/tests/engines.rs @@ -1,8 +1,10 @@ //! 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), and the fuel each counts for a run is the fuel the native engine's -//! own meter counts on the original module. +//! 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; @@ -134,7 +136,7 @@ fn run_all(engine: &dyn Engine, names: &[(String, Vec)]) -> Vec<(String, Resu for (name, args) in names { for c in cases(args) { i.set_fuel(1_000_000); - i.set_depth(0); + i.set_stack(0); let r = i.call(name, &c); out.push((format!("{name}{c:x?}"), r, i.fuel())); } @@ -211,18 +213,19 @@ const PROGRAMS: &str = r#" 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_depth(0); + 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_engines_own_count() { +fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engine_s_own_count_where_no_growth_fails() { 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(); - 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), ("deep", 10), ("deep", 1000), ("multi", 4)]; + // `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); @@ -233,7 +236,10 @@ fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engines_own_count() { 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(); - assert_eq!((a.0.clone().unwrap(), a.1 as u64), theirs[0].clone(), "{name}({arg}): the engine's own meter on the original module"); + // The profile charges the pages a `memory.grow` asks for whether or not it + // gets them (§4.2); the engine's own meter charges them only when it does. + let unmet = if (*name, *arg) == ("mem", 4) { 4 } 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"); } } } @@ -249,7 +255,7 @@ fn a_run_that_runs_out_stops_at_the_same_charge_everywhere() { let go = |e: &dyn Engine| { let mut i = e.instantiate().unwrap(); i.set_fuel(budget); - i.set_depth(0); + i.set_stack(0); let r = i.call(name, &[arg]); (r, i.fuel()) }; @@ -261,14 +267,22 @@ fn a_run_that_runs_out_stops_at_the_same_charge_everywhere() { } } +/// 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_call_depth_is_bounded_the_same_everywhere() { +fn the_stack_ends_at_the_same_call_on_every_engine() { let m = wat::parse_str(PROGRAMS).unwrap(); let prepared = meter::prepare(&m).unwrap(); - let max = wai_wasmq::profile::MAX_CALL_DEPTH as u64; - for (arg, ok) in [(max - 1, true), (max, false), (max * 4, false)] { + // `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})"); + assert_eq!(own.0.is_ok(), ok, "deep({arg}): {:?}", own.0); if !ok { assert_eq!(own.0, Err(Trap::Stack)); } @@ -278,45 +292,103 @@ fn the_call_depth_is_bounded_the_same_everywhere() { } } +/// 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. +fn heavy_frames() -> Vec<(&'static str, Vec)> { + let wasm = |wat: String| wat::parse_str(&wat).unwrap(); + let mut out = Vec::new(); + // 4000 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 = 4000; + 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"); + out.push((if ty == "i64" { "4000 integers held across a call" } else { "4000 floats held across a call" }, wasm(format!("(module (memory 1 1) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", format!("{ty} ").repeat(n))))); + } + // The review's module, at the largest body the profile takes: forty parameters read + // at the end, and a new value held across two dozen calls at every step. + let steps = 8_000; + 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(); + out.push(( + "a value held across two dozen calls at each of 8000 steps", + 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) + )), + )); + // The cheapest value there is to make and keep: the memory's size, two bytes, held + // across calls in rotating locals. + let steps = 15_000; + 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"); + out.push(("the memory's size held across two dozen calls at each of 15000 steps", 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))))); + // 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", + 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", wasm("(module (func $f (export \"f\") (param i64) (result i64) local.get 0 call $f))".to_string()))); + out +} + #[test] -fn the_deepest_frames_the_profile_allows_fit_every_engine() { - // A function that holds 4000 locals live across a call to itself, at the profile's full - // call depth: every frame as large as §3.2 allows, as deep as §4.3 allows. Each local - // is read from memory the callee goes on to write, so a compiler can neither read it - // again after the call nor compute it there: it has to keep all 4000 in the frame. - let n = 4000; - let mut body = String::from("local.get 0 i64.const 4000 i64.rem_u i32.wrap_i64 i32.const 8 i32.mul local.get 0 i64.store "); - for i in 0..n { - body.push_str(&format!("i32.const {} i64.load local.set {} ", 8 * i, i + 1)); +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}"); + } } - body.push_str("local.get 0 i64.eqz if (result i64) i64.const 0 else local.get 0 i64.const 1 i64.sub call $deep end "); - for i in 1..=n { - body.push_str(&format!("local.get {i} i64.add ")); - } - let wat = format!("(module (memory 1 1) (func $deep (param i64) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 call $deep))", "i64 ".repeat(n)); - let m = wat::parse_str(&wat).unwrap(); - assert!(wai_wasmq::profile::check(&m).is_ok()); - let prepared = meter::prepare(&m).unwrap(); - let max = wai_wasmq::profile::MAX_CALL_DEPTH as u64; - let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", max - 1); - assert!(own.0.is_ok(), "{:?}", own.0); - assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", max - 1)); + // 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")))] { - // On a thread with the stack a run has (`run::run`). - let p = prepared.clone(); - let (full, small) = std::thread::Builder::new() - .stack_size(512 << 20) - .spawn(move || { - let full = fuel_of(&wai_wasmq::engine::Native::new(&p).unwrap(), "f", max - 1); - let small = fuel_of(&wai_wasmq::engine::Native::with_stack(&p, 1 << 20).unwrap(), "f", max - 1); - (full, small) - }) - .unwrap() - .join() - .unwrap(); - assert_eq!(own, full, "the native engine, under the stack limit the profile sets"); - // This test can fail: the frames are real, and an engine limit of 1 MiB stops the call. - assert!(matches!(small.0, Err(Trap::Host(_))), "{:?}", small.0); + 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); } } diff --git a/wai/wai-wasmq/tests/fuzz.rs b/wai/wai-wasmq/tests/fuzz.rs new file mode 100644 index 000000000..dda138931 --- /dev/null +++ b/wai/wai-wasmq/tests/fuzz.rs @@ -0,0 +1,245 @@ +//! 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"); +} diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs new file mode 100644 index 000000000..cc9944a15 --- /dev/null +++ b/wai/wai-wasmq/tests/review.rs @@ -0,0 +1,580 @@ +//! 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))"#; + +#[test] +fn a_job_whose_status_or_method_was_edited_outside_the_signature_does_not_verify() { + let (o, fault) = a_job(SPIN, 500); + assert_eq!(fault["status"], "fault"); + assert!(wai_wasmq::verify(&fault, Some(&o), EngineKind::Interpreter).unwrap().ok(), "the positive control"); + let mut edited = fault.clone(); + edited["status"] = json!("done"); + assert!(!wai_wasmq::verify(&edited, Some(&o), EngineKind::Interpreter).unwrap().ok(), "a fault edited to done"); + let (o, done) = a_job(IDENTITY, 500); + for (member, value) in [("status", json!("fault")), ("method", json!("statevector")), ("note", json!("a member nothing signs"))] { + let mut edited = done.clone(); + edited[member] = value; + assert!(!wai_wasmq::verify(&edited, Some(&o), EngineKind::Interpreter).unwrap().ok(), "the body's `{member}` changed"); + assert!(!wai_wasmq::verify(&edited, None, EngineKind::Interpreter).unwrap().ok(), "the body's `{member}` changed, without the bundle"); + } +} + +#[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 +// --------------------------------------------------------------------------- + +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. + let m = rotating(40_000, 1023); + match profile::check(&m) { + // Refused on reading, by a limit the text states. + Err(e) => assert_eq!(e.code, "W010", "{e}"), + Ok(_) => { + let got = call_all(&m, "f", 0); + assert!(got.windows(2).all(|w| w[0] == w[1]), "the engines differ: {:?}", got.iter().map(|g| format!("{:?}", g.0).chars().take(90).collect::()).collect::>()); + } + } +} + +// --------------------------------------------------------------------------- +// 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 183,000 calls of it: inside every rule the + // first text stated, and past an engine's limit once prepared. + let mut body = vec![0x00]; + for _ in 0..183_000 { + 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]); + let m = raw(&[types, section(3, &[0x02, 0x00, 0x01]), section(7, &[0x01, 0x01, b'f', 0x00, 0x01]), section(10, &code)]); + match pack(&bundle(SCALAR, m, vec![])) { + Err(e) => assert_eq!(e.code, "W010", "{e}"), + Ok(env) => { + let jobs = everywhere(&open(&env).unwrap()); + assert!(jobs.iter().all(|j| j.contains("\"status\"")), "packed, and then: {}", &jobs[0][..jobs[0].len().min(160)]); + } + } +} + +#[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/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index af49dfe71..8d63eeef3 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -1,22 +1,40 @@ //! The conformance vectors (`wai/quantum-conformance/wasmq/`) hold on every engine of this -//! build. `WASMQ_BLESS=1` writes them from the reference bundles and the adversarial -//! modules here; without it the test only reads them. +//! 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, Module, Run, pack}; +use wai_wasmq::envelope::{Bundle, Callout, MAGIC, Module, Run, from_parts, pack, parts, write_unchecked}; use wai_wasmq::reference; use wai_wasmq::run::EngineKind; -use serde_json::{Value, json}; fn dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/wasmq") } -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 wasm(wat: &str) -> Vec { + wat::parse_str(wat).unwrap() +} -fn one(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 } +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)> { @@ -29,103 +47,222 @@ fn runs() -> Vec<(&'static str, Bundle)> { job.state = "job".into(); let mut tight = Run::new(32, 9); tight.per_call = 400; - let spin = wat::parse_str(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))"#).unwrap(); - let deep = wat::parse_str(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))"#).unwrap(); - let nan = wat::parse_str(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))"#).unwrap(); - let grow = wat::parse_str(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))"#).unwrap(); + 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))), - ("fault-fuel.wai", one(spin, tight)), - ("fault-depth.wai", one(deep, Run::new(2, 1))), - ("nan-canonical.wai", one(nan, Run::new(8, 2))), - ("grow-past-maximum.wai", one(grow, Run::new(16, 5))), + ("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))), ] } -/// Modules a runner refuses, each as WebAssembly text and its code. -const REFUSE_WAT: &[(&str, &str, &str)] = &[ - ("import-clock", "W002", r#"(module (import "wasi:clocks/monotonic-clock" "now" (func (result i64))) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("import-random", "W002", r#"(module (import "wasi_snapshot_preview1" "random_get" (func (param i32 i32) (result i32))) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("memory-unbounded", "W003", r#"(module (memory 1) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("memory-too-large", "W003", r#"(module (memory 1 65536) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("start-function", "W004", r#"(module (func $s) (start $s) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("shared-memory", "W005", r#"(module (memory 1 1 shared) (func (export "f") (param i64) (result i64) local.get 0))"#), - ("atomic", "W005", r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) i32.const 0 local.get 0 i64.atomic.rmw.add))"#), - ("simd", "W006", r#"(module (func (export "f") (param i64) (result i64) local.get 0 i64x2.splat i64x2.extract_lane 0))"#), - ("relaxed-simd", "W006", r#"(module (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))"#), - ("tail-call", "W007", r#"(module (func $g (param i64) (result i64) local.get 0) (func (export "f") (param i64) (result i64) local.get 0 return_call $g))"#), - ("table-grow", "W007", r#"(module (table 1 funcref) (func (export "f") (param i64) (result i64) ref.null func i32.const 1 table.grow 0 i64.extend_i32_u))"#), - ("table-size", "W008", r#"(module (table 1 funcref) (func (export "f") (param i64) (result i64) table.size 0 i64.extend_i32_u))"#), - ("data-past-memory", "W011", r#"(module (memory 1 1) (data (i32.const 65530) "0123456789") (func (export "f") (param i64) (result i64) local.get 0))"#), - ("reserved-export", "W012", r#"(module (global (export "__wasmq_fuel") (mut i64) (i64.const 0)) (func (export "f") (param i64) (result i64) local.get 0))"#), -]; - -fn bless() { - let d = dir(); - std::fs::create_dir_all(d.join("envelopes")).unwrap(); - std::fs::create_dir_all(d.join("refuse")).unwrap(); - let key = wai_wasmq::vectors::key(); - let mut index_runs = Vec::new(); - for (name, b) in runs() { - let env = pack(&b).unwrap(); - let path = format!("envelopes/{name}"); - std::fs::write(d.join(&path), &env).unwrap(); - let o = wai_wasmq::open(&env).unwrap(); - let (body, _) = wai_wasmq::run(&o, EngineKind::Interpreter, &key, false).unwrap(); - index_runs.push(json!({ "file": path, "envelope_blake3": wai_wasmq::envelope::blake3_hex(&env), "expect": wai_wasmq::vectors::pins(&body) })); - if name == "surface3-decode64.wai" { - std::fs::write(d.join("envelopes/surface3-decode64.job.json"), serde_json::to_string_pretty(&body).unwrap() + "\n").unwrap(); - std::fs::write(d.join("envelopes/surface3-decode64.qasm"), &b.program).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); } - let mut index_refuse = Vec::new(); - for (name, code, wat) in REFUSE_WAT { - let bytes = wat::parse_str(wat).unwrap(); - std::fs::write(d.join(format!("refuse/{name}.wat")), format!("{wat}\n")).unwrap(); - std::fs::write(d.join(format!("refuse/{name}.wasm")), &bytes).unwrap(); - index_refuse.push(json!({ "file": format!("refuse/{name}.wasm"), "code": code })); +} + +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); } - // Envelopes changed after packing: each byte a contract pins, and the bytes around them. - let base = std::fs::read(d.join("envelopes/surface3-decode64.wai")).unwrap(); + 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()); + 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()); + 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.clone(); + 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.clone(); + 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.clone(); - let last = wrong_module.len() - 1; - wrong_module[(module_at + 200).min(last)] ^= 0x40; - let mut trailing = base.clone(); + let mut wrong_module = base.to_vec(); + wrong_module[module_at + 200] ^= 0x40; + let mut trailing = base.to_vec(); trailing.push(0); - for (name, code, bytes) in [ + 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", flip(b"\"shots\":32", b"\"shots\":33")), + ("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()), - ("other-capability", "W013", flip(b"wai.quantum.wasmq", b"wai.quantum.wasmx")), - ] { - std::fs::write(d.join(format!("refuse/{name}.wai")), &bytes).unwrap(); + ] +} + +/// 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())); + 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 })); } - let doc = json!({ + 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, }); - std::fs::write(d.join("vectors.json"), serde_json::to_string_pretty(&doc).unwrap() + "\n").unwrap(); + (files, index) } fn kinds() -> Vec { @@ -133,26 +270,67 @@ fn kinds() -> Vec { } #[test] -fn the_vectors_hold_on_every_engine_of_this_build() { +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() { - bless(); + 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(); } - let (lines, problems) = wai_wasmq::vectors::verify(&dir(), &kinds()).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:#?}"); - let index: Value = serde_json::from_slice(&std::fs::read(dir().join("vectors.json")).unwrap()).unwrap(); assert_eq!(lines.len(), index["runs"].as_array().unwrap().len() + index["refuse"].as_array().unwrap().len()); - assert!(lines.len() >= 30, "{} vectors", lines.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 fault a run vector can show is shown. + let faults: std::collections::BTreeSet<&str> = index["runs"].as_array().unwrap().iter().filter_map(|r| r["expect"]["fault"]["reason"].as_str()).collect(); + for reason in ["fuel_exhausted", "stack_exhausted", "integer_divide_by_zero", "bad_result"] { + assert!(faults.contains(reason), "no run vector ends in {reason}"); + } } #[test] -fn each_refusal_vector_is_refused_only_for_its_defect() { - if std::env::var("WASMQ_BLESS").is_ok() { - return; // the other test is writing the files +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}"); } - // The positive control of every module refusal: the same module without the defect - // passes (the identity function), so each vector's code is its defect's. - assert!(wai_wasmq::profile::check(&wat::parse_str(r#"(module (memory 1 1) (func (export "f") (param i64) (result i64) local.get 0))"#).unwrap()).is_ok()); - // And the envelope refusals' base opens. - assert!(wai_wasmq::open(&std::fs::read(dir().join("envelopes/surface3-decode64.wai")).unwrap()).is_ok()); - let _ = runs; } -- 2.54.0 (Apple Git-157) From da7f4ce9d0dd5857cedb3ed8f4047deb61179f6b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 17:26:06 -0500 Subject: [PATCH 04/17] wasmq: the command line's README runs its wasmQ example, and its translation holds the section The documentation check failed on this branch: the README's wasmQ block neither ran nor said why it could not, and the zh-CN page lacked the section, so its code differed from the English page's. The example now runs on the conformance vectors' run in the open, and the vectors hold the parity checks its decoder reads, so a reader has every file it names. Co-Authored-By: Claude Opus 5.5 --- wai/quantum-conformance/wasmq/README.md | 2 +- .../envelopes/surface3-decode64.checks.bin | Bin 0 -> 87 bytes wai/wai-quantum-cli/README.md | 16 +++++++++------- wai/wai-quantum-cli/README.zh-CN.md | 14 ++++++++++++++ wai/wai-wasmq/tests/vectors.rs | 1 + 5 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 wai/quantum-conformance/wasmq/envelopes/surface3-decode64.checks.bin diff --git a/wai/quantum-conformance/wasmq/README.md b/wai/quantum-conformance/wasmq/README.md index 43aafe328..cff96288b 100644 --- a/wai/quantum-conformance/wasmq/README.md +++ b/wai/quantum-conformance/wasmq/README.md @@ -10,7 +10,7 @@ bytes in that file, which also holds every committed file to the bytes it makes. |---|---| | `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 and its sealed job | +| `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 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 0000000000000000000000000000000000000000..c99e1d527e661c9d9e15dfa9e2f47d25fd166234 GIT binary patch literal 87 zcmXR<%(N;@P1a4$NKMWzX5?gm02V0C2&9>T7{rHRRv?=lh&h0m35Y@RY*03c50VD} DwtNMd literal 0 HcmV?d00001 diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 29d789b47..23d481456 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -293,14 +293,16 @@ 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)): +([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 -wai-quantum check-wasmq decoder.wasm # the module against the profile: nothing imported, memory bounded -wai-quantum pack-wasmq round.qasm --callout decode64=decoder.wasm \ - --prior decoder.wasm=checks.bin --shots 1000 --fuel-per-call 200000 -o round.wai -wai-quantum run round.wai --json > job.json # each call metered; the same bytes on every host -wai-quantum verify job.json --bundle round.wai # the receipt, and the envelope run again and compared +```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 diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index 0212f4668..d00853723 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-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index 8d63eeef3..481a21e78 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -235,6 +235,7 @@ fn made() -> (Vec<(String, Vec)>, Value) { 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)); -- 2.54.0 (Apple Git-157) From 0dabb9f1cc69cd05433ea640c432bd7848437571 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 18:12:23 -0500 Subject: [PATCH 05/17] wasmq: a host can hold and finish what it opens, and verify holds a result to its form under any key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sweep of the families a review found, beyond what it found. Each has a test that failed first, or a measurement; the text, the vectors and the code change together. What fuel does not count - One envelope still had two outcomes. Twenty modules of 256 MiB sealed `ok` on a native host and ended with no receipt on wasm32. A register of a million bits and 4096 shots, under 900 bytes, made a job of 4 GB from 15 GB of memory on a native host and aborted on wasm32. A prior of 1.5 GiB packed on one and aborted the other. - A run could ask for more than any host would finish, at no fuel: a random measurement at the most qubits takes about 4 ms, and a program may hold millions of them, for each of a million shots. - A reader now bounds these from the bundle alone, before anything runs (§2.3, §4.7, §4.8): 128 MiB for an envelope, 4096 pages among a bundle's modules, 2^21 steps for a program, 2^28 bits of results, and 2^42 operations of work, whose weights are measured: a call into a module is 2^16, for the thread the native engine gives it. A test holds the interpreter to every limit at once under wasm32, and at a 64th of the work limit the reference would take at most about two hours at it. A module's old instance is dropped before its next is made. A job under any key - verify called 42 of 45 results this profile never writes "a job the profile writes", when sealed under another key: another shots or seed than the contract's, members the text does not define, fuel that did not add up, a fault of no phase or module. It now holds the request and the result to exactly what §6.4 and §6.2 give, and to the contract. Counts that add past 2^64 no longer panic where overflow is checked. A record - A replay took a shot the run does not have, and shots out of order. It refuses them, and states the record's digest, the one a receipt seals, since each call shows only itself and a record with a call left out still replays. - A transcript's line with a member missing or of another type was read as shot 0 and no fuel. It is refused. A file past a limit is refused before it is read whole. Tests - tests/sweep.rs, and a seeded fuzz of jobs and records in tests/fuzz.rs: a changed job does not hold with its envelope, and neither panics. - 59 vectors: four more refusals, each one defect from one that is accepted. - 56 mutations of the checks are each caught. Four survived a first run because their tests could not fail for the check named, and those tests were repaired. Co-Authored-By: Claude Opus 5.5 --- ...26-11-05-wasmq-hybrid-quantum-classical.md | 8 +- wai/extensions/quantum-wasmq.md | 94 +++- .../memory-past-a-run-s-among-its-modules.wai | Bin 0 -> 1119 bytes .../wasmq/refuse/program-past-its-steps.wai | Bin 0 -> 871 bytes .../refuse/results-past-what-a-host-holds.wai | Bin 0 -> 863 bytes .../refuse/work-past-what-a-run-may-ask.wai | Bin 0 -> 862 bytes wai/quantum-conformance/wasmq/vectors.json | 16 + wai/wai-quantum-cli/README.md | 4 +- wai/wai-quantum-cli/README.zh-CN.md | 2 +- wai/wai-quantum-cli/src/wasmq.rs | 80 ++-- wai/wai-quantum-cli/tests/wasmq.rs | 71 +++ wai/wai-wasmq/README.md | 6 +- wai/wai-wasmq/src/envelope.rs | 61 +++ wai/wai-wasmq/src/profile.rs | 8 +- wai/wai-wasmq/src/run.rs | 303 ++++++++++--- wai/wai-wasmq/tests/fuzz.rs | 124 +++++ wai/wai-wasmq/tests/sweep.rs | 424 ++++++++++++++++++ wai/wai-wasmq/tests/vectors.rs | 15 + 18 files changed, 1090 insertions(+), 126 deletions(-) create mode 100644 wai/quantum-conformance/wasmq/refuse/memory-past-a-run-s-among-its-modules.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/results-past-what-a-host-holds.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/work-past-what-a-run-may-ask.wai create mode 100644 wai/wai-wasmq/tests/sweep.rs diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index 88d5d8b42..157fdad7f 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -14,6 +14,10 @@ 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 fuel does not count.** A reader bounds from the bundle alone what a run asks of its + host outside its modules: the memory among its modules, its results, an envelope's size and + the work of its steps. So an envelope that opens on one host opens and finishes on every + host, a 32-bit one among them, and no small envelope ties a host up unmetered. - **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. The stack is bounded by weight: each function's frame weighs a base and its parameters, results, locals @@ -37,8 +41,8 @@ the text. - **The command line**: `wai-quantum pack-wasmq`, `check-wasmq`, `run ENVELOPE.wai` (`--engine`, `--transcript`, `--replay`), and `verify JOB --bundle ENVELOPE.wai`, which names - the signer and how the run ended, refuses a body edited outside the signature, and runs the - envelope again to compare byte for byte. A bundle goes to no provider: the provider protocol + the signer and how the run ended, holds the body and the result to the form the text gives + under whichever key, and runs the envelope again to compare byte for byte. 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. - **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 diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 9fa4f1ed1..c5a33026d 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -109,21 +109,22 @@ them exactly. Every module is named by at least one callout. A reader MUST refuse an envelope, with the code of §3.3, when: -- 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`); +- 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 program is refused by the reader, calls nothing, has a gate outside §5.1's, or - declares more than 32 768 qubits (`W016`); +- a module breaks the profile (§3: its code), or the modules hold more than 4096 pages + among them (`W003`, §4.7); +- the program is refused by the reader, calls nothing, has a gate outside §5.1's, + declares more than 32 768 qubits, or takes more than 2^21 steps (`W016`, §4.8); - 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 (`W017`). +- 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). @@ -186,7 +187,7 @@ prepares a module at all of them at once (`wai-wasmq/src/meter.rs`). |---|---| | `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 | +| `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 | @@ -196,11 +197,11 @@ prepares a module at all of them at once (`wai-wasmq/src/meter.rs`). | `W010` | is past a size the last rows of §3.2 give | | `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 (§2.3) | +| `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 the reader refuses, that calls nothing, with a gate outside §5.1's, or of more than 32 768 qubits | -| `W017` | (envelope) has a run outside the profile's ranges | +| `W016` | (envelope) has a program the reader refuses, that calls nothing, with a gate outside §5.1's, of more than 32 768 qubits or of more than 2^21 steps | +| `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. @@ -346,6 +347,50 @@ maximum, it succeeds: a host that cannot grow the memory within the maximum (it 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 fuel does not count + +Fuel is the work of the modules. A run also asks work and memory of its host that no +fuel counts: the program's steps on the tableau, a fresh instance's memory, the result. +So that every host can finish what it opens, a reader bounds them from the bundle alone, +before anything runs, and refuses past them (`W017`); nothing here differs by engine and +nothing here is in a receipt. + +**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 program takes at most 2^21 steps, its loops unrolled (`W016`), since a reader +holds each of them for the run. A run's shots times the bits of a shot are at most 2^28 +(`W017`): the bits of a shot are the program's, each of them a character of an outcome in +`counts`. With the most memory (§4.7), the largest envelope (§2.3) and the most qubits, 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. + +**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, those the reader adds to a program that measures nothing among them | `n`·`r` | +| each callout | 2^16 + ⌈`b`/64⌉, for `b` the bits of its arguments and result | +| 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`, `wasmq-prior` and `init` | 2^16 | +| each page of the memory a module starts with | 8192 | + +The 2^16 is the host's part of a call: on the reference's native engine, a thread of the +call's own. 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 work (§4.2). Measured at a 64th of the limit, the +reference would take at most about two hours at it. + ## 5. Callouts ### 5.1 In the program @@ -485,7 +530,7 @@ A host that could not run the bundle seals nothing: a refused envelope (§2.3, 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 or sealed as a fault. +what a bundle could cause is refused on reading (§2.3, §4.7, §4.8) or sealed as a fault. ### 6.4 The result @@ -530,10 +575,17 @@ the receipt's digest is the same everywhere, and under one key the whole job is. reader's to compare with the signer it expects; - that the body is one of this profile: its four members and no other, `status` and `method` as the signed result gives them, and no whole number past 2^53 − 1; -- that the result is one this profile writes: fuel within the contract's budgets, each - callout's largest call within the call budget, the modules the contract pins, a fault - reason §6.2 names, a run that is `ok` only after all its shots, counts that sum to the - shots run. +- that the request and the result are ones this profile writes, under whichever key: + exactly the members §6.4 and §6.2 give, each of its form; the format, the profile and + the unmetered energy §6.4 gives; the method, shots, seed, budgets and modules the + contract gives, and no callout it does not name; fuel within its budgets that is what the + instances and the calls add up to, and callouts that are the calls the fuel counts; a run + that is `ok` only after all its shots, or a fault in the shot after those that finished, + of a reason, a phase and a budget §6.2 names; and counts of whole outcomes of one width, + in bits, that sum to the shots run. + +Without the envelope this is all a check can hold a result to: a key can seal any result +of this form. Only running the envelope again shows that a result is the one it gives. It prints how the run ended. With `--bundle ENVELOPE.wai` it also checks that the envelope is the one the receipt binds, and runs it again here: the result must be the @@ -558,8 +610,11 @@ that runs classical code between gates, and what does not: 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`). A machine that ran the - module would have to report those inputs and outputs for anyone to check them. + 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. @@ -604,7 +659,8 @@ feasible fills the target, and atoms are moved, never made or lost. - `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; + 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). `wasmq_vectors verify wai/quantum-conformance/wasmq` checks them on every engine of the 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 0000000000000000000000000000000000000000..38ef65f25c449b86957238ecf4017929c0b634ba GIT binary patch literal 1119 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 job.json # 每次调用 wai-quantum verify job.json --bundle round.wai # 签名者、运行如何结束,以及重新运行封装并比较 ``` -这一版本运行 Clifford 程序:不带角度的门、测量、重置和以比特为条件的操作。超出燃料、递归超出栈或发生陷阱的调用,会使运行以一份指明故障的回执结束,该回执仍可验证。`verify` 指明签名者以及运行如何结束。`run --engine native|interpreter` 选择引擎;回执相同。`run --transcript FILE` 记录每次调用的输入、输出和燃料,`run round.wai --replay FILE` 仅在一份记录上运行模块并检查每次调用。量子部分在此处模拟:封装不会发往任何提供方,提供方的程序不携带门之间的调用。 +这一版本运行 Clifford 程序:不带角度的门、测量、重置和以比特为条件的操作。超出燃料、递归超出栈或发生陷阱的调用,会使运行以一份指明故障的回执结束,该回执仍可验证。`verify` 指明签名者以及运行如何结束。`run --engine native|interpreter` 选择引擎;回执相同。`run --transcript FILE` 记录每次调用的输入、输出和燃料,`run round.wai --replay FILE` 仅在一份记录上运行模块、检查每次调用,并给出该记录的摘要,即回执所封存的摘要。量子部分在此处模拟:封装不会发往任何提供方,提供方的程序不携带门之间的调用。 ## 脉冲程序 diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index 1d8a00bb9..f18ec9fc4 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -19,6 +19,24 @@ 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)"; @@ -93,7 +111,7 @@ pub fn pack(a: &PackArgs, inputs: wai_quantum::qasm::Inputs) -> ! { if let Some(i) = modules.iter().position(|(p, _)| *p == path) { return i; } - let bytes = read(&path); + let bytes = read_module(&path); modules.push((path.clone(), Module { name: module_name(&path), bytes, priors: vec![] })); modules.len() - 1 }; @@ -106,7 +124,7 @@ pub fn pack(a: &PackArgs, inputs: wai_quantum::qasm::Inputs) -> ! { 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(Path::new(prior)); + 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 }; @@ -130,43 +148,51 @@ pub fn pack(a: &PackArgs, inputs: wai_quantum::qasm::Inputs) -> ! { 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(file)).unwrap_or_else(|r| die(&r.to_string())); + 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 { let text = String::from_utf8(read(path)).unwrap_or_else(|_| die("the transcript is not UTF-8")); - let unhex = |s: &str| { - let digit = |c: u8| (c as char).to_digit(16).unwrap_or_else(|| die("a transcript's bytes are pairs of hexadecimal digits")) as u8; - if !s.len().is_multiple_of(2) { - die("a transcript's bytes are pairs of hexadecimal digits"); - } - s.as_bytes().chunks(2).map(|p| digit(p[0]) << 4 | digit(p[1])).collect::>() - }; - let calls: Vec = text - .lines() - .filter(|l| !l.trim().is_empty()) - .map(|l| { - let v: Value = serde_json::from_str(l).unwrap_or_else(|e| die(&format!("the transcript: {e}"))); - wai_wasmq::run::Call { - shot: v["shot"].as_u64().unwrap_or(0), - callout: v["callout"].as_str().unwrap_or("").to_string(), - input: unhex(v["input"].as_str().unwrap_or("")), - output: unhex(v["output"].as_str().unwrap_or("")), - fuel: v["fuel"].as_u64().unwrap_or(0), - } - }) - .collect(); + let calls: Vec = text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty()).map(|(k, l)| record_of(k + 1, l)).collect(); let problems = wai_wasmq::run::replay(&o, &calls, kind).unwrap_or_else(|e| die(&e.to_string())); + // 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. + let digest = wai_wasmq::run::transcript_blake3(&o, &calls); if json_out { - println!("{}", json!({ "calls": calls.len(), "problems": problems })); + println!("{}", json!({ "calls": calls.len(), "problems": problems, "transcript_blake3": digest })); } else { println!("replayed {} calls through the envelope's modules", calls.len()); if problems.is_empty() { println!("result each call's output and fuel as the transcript records"); } + if let Some(d) = &digest { + println!("transcript blake3 {d}, which a receipt of the run that made this record seals, if it ended ok"); + } for p in &problems { println!("differs {p}"); } @@ -216,7 +242,7 @@ fn print_job(body: &Value, top: usize, kind: EngineKind) { /// `verify` for a wasmQ job: exit status 0 when it holds. pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { - let opened = bundle.map(|p| wai_wasmq::open(&read(p)).unwrap_or_else(|r| die(&r.to_string()))); + let opened = bundle.map(|p| wai_wasmq::open(&read_envelope(p)).unwrap_or_else(|r| die(&r.to_string()))); let verdict = wai_wasmq::verify(v, opened.as_ref(), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); println!("signature {}", if verdict.signature { "VALID" } else { "INVALID" }); println!("binds the contract (program, modules, priors, run) and the result: fuel, modules, transcript, fault"); @@ -250,7 +276,7 @@ pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { } pub fn check(module: &Path, json_out: bool) -> ! { - let bytes = read(module); + 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) => { diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index be81fb16e..a9bb1bc1d 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -199,3 +199,74 @@ fn pack_wasmq_takes_a_generator_s_inputs() { } 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"); + 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)); +} diff --git a/wai/wai-wasmq/README.md b/wai/wai-wasmq/README.md index be2ed78da..cbcccda18 100644 --- a/wai/wai-wasmq/README.md +++ b/wai/wai-wasmq/README.md @@ -13,10 +13,12 @@ functions and their priors, pinned by BLAKE3. This crate: 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`), runs a bundle of Clifford gates shot by shot on +- 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 `wai-quantum verify` checks; -- replays a record of calls through the modules alone (`run::replay`); +- 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). diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index 30cd42126..ab75b73ab 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -28,6 +28,18 @@ pub const MAX_EXACT: u64 = (1 << 53) - 1; pub const MAX_QUBITS: u32 = 1 << 15; /// The one method of this version of the profile (§2.2). pub const METHOD: &str = "stabilizer"; +/// The most bytes an envelope holds (§2.3): a reader holds them and its frames at once, +/// and what one host opens every host must be able to. +pub const MAX_ENVELOPE_BYTES: usize = 1 << 27; +/// 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 a run's results may hold: its shots times the bits of a shot (§4.8). +pub const MAX_RESULT_BITS: u128 = 1 << 28; +/// The most work a run may ask of its host outside its modules (§4.8). +pub const MAX_HOST_WORK: u128 = 1 << 42; +/// The host's part of a call into a module, or of making an instance, as [`host_work`] +/// counts it: on the native engine, about 50 microseconds, for a thread of its own. +const CALL_WORK: u128 = 1 << 16; pub fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() @@ -112,12 +124,44 @@ pub fn read(program: &str, inputs: &wai_quantum::qasm::Inputs) -> Result<(wai_qu if p.n_qubits > MAX_QUBITS { return Err(refuse("W016", format!("{} qubits; the method holds at most {MAX_QUBITS}", p.n_qubits))); } + if h.steps.len() > MAX_STEPS { + return Err(refuse("W016", format!("{} steps, its loops unrolled; a program takes at most {MAX_STEPS}", h.steps.len()))); + } Ok((h, METHOD.to_string())) } +/// The work a run asks of its host that no fuel counts (§4.8), in operations on a word: +/// for each shot a fresh tableau and the program's steps, and for each instance the calls +/// that make it and its memory. (Fuel counts a prior's bytes, as it does a module's work.) +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(_)) => n * r, + Step::Callout { input, output, .. } => CALL_WORK + ((input.len() + output.len()) as u128).div_ceil(64), + Step::Instr(_) => 1, + }; + } + let mut instance = 0; + for (m, f) in b.modules.iter().zip(facts) { + let calls = 1 + m.priors.len() + usize::from(f.functions.contains_key("wasmq-seed")) + usize::from(f.functions.contains_key("init")); + instance += CALL_WORK * calls as u128 + f.memory.map_or(0, |(initial, _)| u128::from(initial)) * 8192; + } + let instances = if b.run.state == "shot" { b.run.shots } else { 1 }; + u128::from(b.run.shots) * shot + u128::from(instances) * instance +} + /// 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 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 { @@ -144,6 +188,20 @@ pub fn check(b: &Bundle) -> Result { // 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 on the host's part, which no fuel counts; 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( @@ -325,6 +383,9 @@ fn frame<'a>(payload: &'a [u8], at: &mut usize, spec: &Value, what: &str, code: /// 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")); diff --git a/wai/wai-wasmq/src/profile.rs b/wai/wai-wasmq/src/profile.rs index d6bfe246a..2460a047d 100644 --- a/wai/wai-wasmq/src/profile.rs +++ b/wai/wai-wasmq/src/profile.rs @@ -69,7 +69,7 @@ impl std::fmt::Display for Refusal { 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, or a maximum past the profile's"), + ("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"), @@ -79,11 +79,11 @@ pub const CODES: &[(&str, &str)] = &[ ("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, a contract that is not canonical or holds other members than the profile's, or frames that do not add up"), + ("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, or more qubits than the method holds"), - ("W017", "the run: shots, seed, an input or a fuel budget outside what the profile and a receipt can hold"), + ("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. diff --git a/wai/wai-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs index d63841443..d86b1f84c 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -19,6 +19,8 @@ use wai_quantum::quantum_tableau::{Clifford, Tableau}; /// The result's format. pub const FORMAT: &str = "wai.quantum.wasmq.result/1"; +/// The members of a result (§6.4), `fault` aside, which a run that stopped has. +pub const RESULT: [&str; 16] = ["format", "profile", "status", "method", "qubits", "shots", "shots_run", "seed", "counts", "callouts", "fuel", "modules", "transcript_blake3", "accuracy", "energy", "receipt"]; /// What a result's `energy` says (§6.4): no meter reads a run. pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; /// The members of a job body (§6.4). Only `request` and `result` are under the signature; @@ -208,8 +210,33 @@ pub fn run(opened: &Opened, kind: EngineKind, key: &SigningKey, log: bool) -> Re execute(opened, &engines, key, log) } +fn transcript_start() -> blake3::Hasher { + let mut t = blake3::Hasher::new(); + t.update(b"wai:wasmq-transcript\x01"); + 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. -fn width(p: &Program, steps: &[Step]) -> usize { +pub(crate) fn width(p: &Program, steps: &[Step]) -> usize { let mut w = p.n_bits as usize; for s in steps { match s { @@ -242,8 +269,7 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key 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 = blake3::Hasher::new(); - transcript.update(b"wai:wasmq-transcript\x01"); + let mut transcript = transcript_start(); let mut record = log.then(Vec::new); let mut counts: BTreeMap = BTreeMap::new(); let mut shots_run = 0u64; @@ -255,6 +281,8 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key 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; @@ -323,9 +351,7 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key for (k, &bit) in output.iter().enumerate() { bits[bit as usize] = outs[k]; } - for part in [&shot.to_le_bytes()[..], &func.to_le_bytes(), &(in_bytes.len() as u32).to_le_bytes(), &in_bytes, &(out_bytes.len() as u32).to_le_bytes(), &out_bytes, &n.to_le_bytes()] { - transcript.update(part); - } + hash_call(&mut transcript, shot, *func, &in_bytes, &out_bytes, n); if let Some(l) = record.as_mut() { l.push(Call { shot, callout: decl.name.clone(), input: in_bytes, output: out_bytes, fuel: n }); } @@ -403,7 +429,17 @@ pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result)>> = engines.iter().map(|_| None).collect(); let mut problems = Vec::new(); 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.iter().enumerate() { + if call.shot >= b.run.shots { + problems.push(format!("call {k}: shot {}, where the run has {} shots", call.shot, b.run.shots)); + continue; + } + if call.shot < last { + problems.push(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 { problems.push(format!("call {k}: the program declares no `extern {}`", call.callout)); continue; @@ -415,6 +451,7 @@ pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result 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) => { @@ -472,6 +509,196 @@ pub fn seal(opened: &Opened, mut result: Value, key: &SigningKey) -> Value { json!({ "status": status, "method": result["method"], "request": req, "result": result }) } +/// Holds `v` to exactly these members: a message for each it lacks, or holds besides. +fn members(v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mut Vec) { + let Some(o) = v.as_object() else { + out.push(format!("{what} is not an object")); + return; + }; + for name in required { + if !o.contains_key(*name) { + out.push(format!("{what} has no `{name}`")); + } + } + for name in o.keys() { + if !required.contains(&name.as_str()) && !optional.contains(&name.as_str()) { + out.push(format!("{what} holds `{name}`, which the text does not define")); + } + } +} + +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'))) +} + +/// What makes a job other than one this profile writes (§6.4, §6.5). A signature shows +/// that a key sealed a job, and says nothing of what the job holds: so without the +/// envelope to run again, this is what holds a result to its contract and to itself. +fn form(job: &Value) -> Vec { + let mut out = Vec::new(); + let (req, r) = (&job["request"], &job["result"]); + // The body: nothing in it but what the signature covers, or repeats from under it. + 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 body's status is {value}, where its signed result says {}", body_status(r))), + "method" if value == &r["method"] => {} + "method" => out.push(format!("the body's method is {value}, where its signed result says {}", r["method"])), + other => out.push(format!("the body holds `{other}`, which no signature covers")), + } + } + for name in BODY { + if job.get(name).is_none() { + out.push(format!("the body has no `{name}`")); + } + } + // A number a double cannot hold would be read differently by readers of the receipt. + for (what, value) in [("request", req), ("result", r)] { + if let Some(at) = wai_quantum::engine::receipt::inexact_number(value) { + out.push(format!("the {what}'s `{at}` is a whole number past 2^53 - 1")); + } + } + members(req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); + if !is_digest(&req["envelope_blake3"]) { + out.push(format!("the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); + } + members(r, "the result", &RESULT, &["fault"], &mut out); + let c = &req["wasmq"]; + let run = &c["run"]; + // What the result repeats from the contract, or fixes. + let fixed = [("format", json!(FORMAT)), ("profile", json!(profile::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 r[name] != want { + out.push(format!("the result's {name} is {}, not {want}", r[name])); + } + } + if r["qubits"].as_u64().is_none_or(|q| q > u64::from(crate::envelope::MAX_QUBITS)) { + out.push(format!("the result's qubits are {}", r["qubits"])); + } + if !is_digest(&r["transcript_blake3"]) { + out.push(format!("the result's transcript_blake3 is {}, not a digest", r["transcript_blake3"])); + } + // The fuel: within its budgets, and what the calls and the instances add up to. + let fuel = &r["fuel"]; + members(fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); + if fuel["budget"] != run["fuel"] { + out.push(format!("the fuel's budget is {}, where the contract gives {}", fuel["budget"], run["fuel"])); + } + let per_job = run["fuel"]["per_job"].as_u64().unwrap_or(0); + if fuel["used"].as_u64().is_none_or(|u| u > per_job) { + out.push(format!("fuel used {} is past the run's budget {per_job}", fuel["used"])); + } + let per_call = run["fuel"]["per_call"].as_u64().unwrap_or(0); + let externs: Vec<&Value> = c["callouts"].as_array().into_iter().flatten().map(|x| &x["extern"]).collect(); + let mut spent = fuel["instances"].as_u64(); + let mut calls = Some(0u64); + for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { + members(f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); + if !externs.iter().any(|e| e.as_str() == Some(name)) { + out.push(format!("fuel for {name}, a callout the contract does not name")); + } + if f["max"].as_u64().is_none_or(|m| m > per_call) { + out.push(format!("callout {name} used {} in one call, past the budget {per_call}", f["max"])); + } + if f["max"].as_u64().is_none_or(|m| f["fuel"].as_u64().is_none_or(|all| m > all)) { + out.push(format!("callout {name}'s largest call, {}, is past its fuel, {}", f["max"], f["fuel"])); + } + if f["calls"].as_u64().is_none_or(|n| n == 0) { + out.push(format!("callout {name} has fuel for {} calls", f["calls"])); + } + spent = spent.zip(f["fuel"].as_u64()).and_then(|(a, b)| a.checked_add(b)); + calls = calls.zip(f["calls"].as_u64()).and_then(|(a, b)| a.checked_add(b)); + } + if spent != fuel["used"].as_u64() { + out.push(format!("fuel used {}, where the instances and the calls add up to {spent:?}", fuel["used"])); + } + if calls != r["callouts"].as_u64() { + out.push(format!("{} callouts, where the fuel counts {calls:?}", r["callouts"])); + } + // The modules the contract pins. + let mods = c["modules"].as_array().cloned().unwrap_or_default(); + let got = r["modules"].as_array().cloned().unwrap_or_default(); + if mods.len() != got.len() || mods.iter().zip(&got).any(|(a, b)| a["blake3"] != b["blake3"] || a["name"] != b["name"]) { + out.push("the result's modules are not the ones the contract pins".into()); + } + for m in &got { + members(m, "a module of the result", &["name", "blake3"], &[], &mut out); + } + // How it ended. + let (shots, shots_run) = (r["shots"].as_u64(), r["shots_run"].as_u64()); + match r["status"].as_str() { + Some("ok") => { + if r.get("fault").is_some() { + out.push("a result that is ok and names a fault".into()); + } + if shots_run != shots { + out.push(format!("a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); + } + } + Some("fault") => { + let f = &r["fault"]; + members(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!("a fault the profile does not name: {}", f["reason"])); + } + if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) { + out.push(format!("a fault after {} of {} shots", r["shots_run"], r["shots"])); + } + if f["shot"].as_u64() != shots_run { + out.push(format!("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!("a fault in a phase the profile does not name: {}", f["phase"])); + } + if !mods.iter().any(|m| m["name"] == f["module"]) { + out.push(format!("a fault in {}, a module the contract does not name", f["module"])); + } + if f["fuel"].as_u64().is_none_or(|n| fuel["used"].as_u64().is_none_or(|all| n > all)) { + out.push(format!("a fault after {} fuel, of {} used", f["fuel"], fuel["used"])); + } + let is_call = phase == Some("call"); + match f.get("callout") { + Some(x) if !is_call || !externs.contains(&x) => out.push(format!("a fault that names the callout {x} in phase {}", f["phase"])), + None if is_call => out.push("a call's fault that names no callout".into()), + _ => {} + } + match f.get("budget") { + Some(x) if reason != Some("fuel_exhausted") => out.push(format!("a budget, {x}, on a fault of {}", f["reason"])), + Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), + None if reason == Some("fuel_exhausted") => out.push("a fuel fault that names no budget".into()), + _ => {} + } + } + _ => out.push(format!("status {}", r["status"])), + } + // 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!("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!("an outcome that is not bits: {outcome:?}")); + } + if *width.get_or_insert(outcome.len()) != outcome.len() { + out.push(format!("outcomes of {} and {} bits", width.unwrap_or(0), outcome.len())); + } + if n.as_u64().is_none_or(|n| n == 0) { + out.push(format!("a count of {n} for {outcome:?}")); + } + total = total.zip(n.as_u64()).and_then(|(a, b)| a.checked_add(b)); + } + if total != shots_run { + out.push(format!("counts sum to {total:?}, not the {} shots run", r["shots_run"])); + } + out +} + /// What [`verify`] found. #[derive(Debug, Default)] pub struct Verdict { @@ -505,70 +732,8 @@ impl Verdict { pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result { let (signature, key) = wai_quantum::engine::job::verify(job)?; let r = &job["result"]; - let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), ..Verdict::default() }; - let mut problem = |s: String| v.well_formed.push(s); - // The body: nothing in it but what the signature covers, or repeats from under it. - for (name, value) in job.as_object().into_iter().flatten() { - match name.as_str() { - "request" | "result" => {} - "status" if value == body_status(r) => {} - "status" => problem(format!("the body's status is {value}, where its signed result says {}", body_status(r))), - "method" if value == &r["method"] => {} - "method" => problem(format!("the body's method is {value}, where its signed result says {}", r["method"])), - other => problem(format!("the body holds `{other}`, which no signature covers")), - } - } - for name in BODY { - if job.get(name).is_none() { - problem(format!("the body has no `{name}`")); - } - } - // A number a double cannot hold would be read differently by readers of the receipt. - for (what, value) in [("request", &job["request"]), ("result", r)] { - if let Some(at) = wai_quantum::engine::receipt::inexact_number(value) { - problem(format!("the {what}'s `{at}` is a whole number past 2^53 - 1")); - } - } - if r["format"] != FORMAT { - problem(format!("the result's format is {}, not {FORMAT}", r["format"])); - } let c = &job["request"]["wasmq"]; - let fuel = &r["fuel"]; - let per_job = c["run"]["fuel"]["per_job"].as_u64().unwrap_or(0); - if fuel["used"].as_u64().is_none_or(|u| u > per_job) { - problem(format!("fuel used {} is past the run's budget {per_job}", fuel["used"])); - } - let per_call = c["run"]["fuel"]["per_call"].as_u64().unwrap_or(0); - for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { - if f["max"].as_u64().is_none_or(|m| m > per_call) { - problem(format!("callout {name} used {} in one call, past the budget {per_call}", f["max"])); - } - } - let mods = c["modules"].as_array().cloned().unwrap_or_default(); - let got = r["modules"].as_array().cloned().unwrap_or_default(); - if mods.len() != got.len() || mods.iter().zip(&got).any(|(a, b)| a["blake3"] != b["blake3"] || a["name"] != b["name"]) { - problem("the result's modules are not the ones the contract pins".into()); - } - match r["status"].as_str() { - Some("ok") => { - if r.get("fault").is_some() { - problem("a result that is ok and names a fault".into()); - } - if r["shots_run"] != r["shots"] { - problem(format!("a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); - } - } - Some("fault") => { - if !r["fault"]["reason"].as_str().is_some_and(|x| REASONS.contains(&x)) { - problem(format!("a fault the profile does not name: {}", r["fault"]["reason"])); - } - } - _ => problem(format!("status {}", r["status"])), - } - let total: u64 = r["counts"].as_object().into_iter().flatten().filter_map(|(_, n)| n.as_u64()).sum(); - if Some(total) != r["shots_run"].as_u64() { - problem(format!("counts sum to {total}, not the {} shots run", r["shots_run"])); - } + let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), well_formed: form(job), ..Verdict::default() }; if let Some(o) = bundle { let same = o.envelope_blake3 == job["request"]["envelope_blake3"] && &o.contract == c; v.bundle = Some(same); diff --git a/wai/wai-wasmq/tests/fuzz.rs b/wai/wai-wasmq/tests/fuzz.rs index dda138931..aa7bc3abf 100644 --- a/wai/wai-wasmq/tests/fuzz.rs +++ b/wai/wai-wasmq/tests/fuzz.rs @@ -243,3 +243,127 @@ fn a_changed_envelope_or_program_is_refused_without_a_panic() { } 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 through. + let v = catch_unwind(AssertUnwindSafe(|| verify(&e, Some(&o), k))).unwrap_or_else(|_| panic!("verify panicked on {e}")); + 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/sweep.rs b/wai/wai-wasmq/tests/sweep.rs new file mode 100644 index 000000000..5d6fadbdb --- /dev/null +++ b/wai/wai-wasmq/tests/sweep.rs @@ -0,0 +1,424 @@ +//! 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_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: a register no step writes, in a program +/// that measures nothing, is not in its result (the reader measures every qubit instead). +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 << 28, ONE_MEASURED), wasm(IDENTITY), 1)), None, "2^28 bits in one shot"); + assert_eq!(code(&one(program(1, (1 << 28) + 1, ONE_MEASURED), wasm(IDENTITY), 1)), Some("W017"), "one bit more"); +} + +#[test] +fn a_run_no_host_could_finish_is_refused_though_it_spends_no_fuel() { + // A random measurement at the most qubits takes about 4 ms and no fuel, so 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 program that measures nothing is measured at its end, every qubit, and that counts. + assert_eq!(code(&one(program(32768, 2, ""), wasm(IDENTITY), 1 << 12)), Some("W017"), "the measurements the reader adds"); + // 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"); + // And the same programs at a size a host finishes pack. + assert_eq!(code(&one(program(32768, 2, measures), wasm(IDENTITY), 1)), None, "these measurements, once"); + assert_eq!(code(&one(program(32768, 2, ONE_MEASURED), wasm(IDENTITY), 1 << 12)), None, "these tableaux, four thousand times"); + // A call costs the native engine about 50 microseconds, whatever fuel it uses. + let calls = "for uint i in [0:99] { d = f(d); }\nc[0] = measure q[0];\n"; + assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 20)), Some("W017"), "a hundred calls, a million times"); + assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 10)), None, "a hundred calls, a thousand times"); +} + +#[test] +fn the_work_a_run_asks_of_its_host_is_the_text_s_sum() { + // §4.8, by hand: 64 qubits take r = 2 words a qubit. A tableau is 2 * 64 * 2 = 256; the + // call is 2^16 + 1; a gate is 2, twice; the measurement is 64 * 2 = 128; the jump is 1. + // 65926 for a shot. Three pages are 3 * 8192 = 24576, and making an instance 2^16. + 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 mut b = one(src.clone(), m, 5); + assert_eq!(work(&b), 5 * 65926 + 5 * 90112, "an instance for each shot"); + b.run.state = "job".into(); + assert_eq!(work(&b), 5 * 65926 + 90112, "one instance for the run"); + // A seed, an init and a prior are three more calls. (Fuel counts the prior's bytes.) + let mut b = one(src, 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))"#), 5); + b.modules[0].priors.push(("p".into(), vec![0; 17])); + assert_eq!(work(&b), 5 * 65926 + 5 * (90112 + 3 * 65536)); +} + +#[test] +fn the_work_limit_admits_its_largest_and_refuses_one_more() { + // 2^20 shots, so a shot and an instance may take 2^22 together. A tableau, the call and + // the measurement of one qubit are 2 + 65537 + 1, making the instance 65536, and 495 + // pages 4055040: which leaves 8188 for gates of one word. + let m = wasm(r#"(module (memory (export "memory") 495 495) (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 (h, _) = wai_wasmq::envelope::read(&with(8188).program, &Default::default()).unwrap(); + assert_eq!(wai_wasmq::envelope::host_work(&h, &[wai_wasmq::profile::check(&m).unwrap()], &with(8188)), wai_wasmq::envelope::MAX_HOST_WORK); + assert_eq!(code(&with(8188)), None, "exactly the limit"); + assert_eq!(code(&with(8189)), 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"); +} + +/// 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 bundle at every limit a run holds in memory: the most memory, a prior of nearly all an +/// envelope holds, the most qubits and the most result, with two instances, and `calls` more. +fn the_largest(calls: u64) -> wai_wasmq::Opened { + let m = r#"(module (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 body = format!("h q[0];\nc[0] = measure q[0];\nreset q[0];\nfor uint i in [0:{}] {{ d = f(d); }}\n", calls - 1); + let mut b = one(program(32768, 1 << 27, &body), wasm(m), 2); + b.modules[0].priors.push(("long".into(), vec![1; (1 << 27) - (1 << 16)])); + // 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 as well, on the interpreter, which is the engine wasm32 has: + // the native engine's thread for each of two million calls would only take minutes. + let o = the_largest((1 << 21) - 8); + assert_eq!(o.contract["run"]["shots"], 2); + let (job, _) = run(&o, EngineKind::Interpreter, &SigningKey::from_bytes(&[7; 32]), false).unwrap(); + assert_eq!(job["result"]["status"], "ok", "{}", job["result"]["fault"]); +} + +#[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, 1, "h q;\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); + }), +]; + +#[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"); +} diff --git a/wai/wai-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index 481a21e78..bdf92d342 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -176,6 +176,21 @@ fn bundle_refusals() -> Vec<(&'static str, &'static str, Bundle, Bundle)> { 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)); + let wide = |bits: u32| PROGRAM.replace("bit[8] r;", &format!("bit[8] r;\nbit[{bits}] pad;")); + add("results-past-what-a-host-holds", "W017", one(&wide(1 << 20), wasm(IDENTITY), Run::new(1 << 8, 1)), one(&wide((1 << 20) - 16), 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];"); + let calls = |n: u64| PROGRAM.replace("r = f(b);", &format!("for uint i in [0:{}] {{ r = f(b); }}", n - 1)); + add("program-past-its-steps", "W016", one(&calls(1 << 21), wasm(IDENTITY), Run::new(1, 1)), one(&calls((1 << 21) - 16), 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 } -- 2.54.0 (Apple Git-157) From c149358858b945ccb99f3e579294076d9231446f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 6 Nov 2026 07:59:32 -0500 Subject: [PATCH 06/17] wasmq: a reader bounds a program as it reads it, verify holds the contract and the seal to their forms, and every fault has a vector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second review of #659 found ways for one envelope to still have two outcomes, for a host to be asked more than it could do before anything was refused, and for verify to call a job of any contract one of the profile's. Each has a test that failed on the reviewed code and passes here; the text, the vectors and the code change together. The reader - A program is read within bounds checked as it is read (§4.8): its text (1 MiB), its nesting (128, for every reading of a program: a bracket nested 100,000 deep overflowed the stack), its qubits, its bits and any one value's (2^20), its steps (2^21), the bits its calls pass (2^23), and the work of reading it (2^26). On the reviewed code a one-line program held 4 GiB, another 2 GiB, a third took twelve minutes to read, and none was refused. - A program that measures nothing has its own bits as its outcome; no qubit is read for it. What a run asks of its host - The modules of a bundle hold at most 512 KiB among them, a body at most 16 KiB: thirty modules of 4 MiB held gigabytes on any engine, and the native engine compiled a 3 MB body with 1.6 GB. The interpreter prepares nothing it does not run. - The work counts each call a callout makes into its module, its bits, each instance's calls and bytes, the modules' compiling, and the most fuel a run may be given. A page a module grows is charged 1024 fuel, as zeroing it costs; the default run budget is 2^34. - A determined measurement is the sign of the stabilizers' product, found a machine word of rows at a time: 6 ms where it took 9.6 s at the most qubits, the same outcome. - The native engine makes a run's calls on one thread, not a thread for each. - A frame's weight counts the operands its body holds: 3840 results of calls held across a call ran the native stack out before the profile's limit. verify - A job is read from its text strictly: a member twice, or a number a double does not hold exactly, is refused. The contract is held to §2.2, the receipt to the seal's form, and the fuel to what the instances, the calls and a fault's phase could be given. The seal is checked by the receipt alone, and only in its form: a key of other characters panicked. - The MCP server's verify_quantum_result judges a wasmQ job as the command line does. - --history is refused for a wasmQ job; a program's flags are refused with an envelope, and an envelope's with a program, where each was ignored. A transcript is written and replayed a call at a time, its lines bounded, and a replay states the record's digest. Vectors and tests - 20 more run vectors: every fault reason, phase and budget, cabi_post, each way a list result is not one, a signed argument, two modules made in order, a program that measures nothing. A refusal's line carries the code the host computed. - tests/reading.rs counts what a reader holds; the review tests reach the engines they name; a module at the profile's limits is compiled and run on every engine. - mutations.py removes each of 81 checks in turn and names the test that then fails. Co-Authored-By: Claude Opus 5.5 --- ...26-11-05-wasmq-hybrid-quantum-classical.md | 31 +- wai/extensions/quantum-wasmq.md | 178 +++--- wai/quantum-conformance/wasmq/README.md | 2 +- .../wasmq/envelopes/atoms4x4-plan64.wai | Bin 25364 -> 25362 bytes .../wasmq/envelopes/atoms9x9-plan.wai | Bin 25376 -> 25374 bytes .../wasmq/envelopes/fault-bad-result.wai | Bin 845 -> 843 bytes .../wasmq/envelopes/fault-fuel-instance.wai | Bin 0 -> 852 bytes .../wasmq/envelopes/fault-fuel-run.wai | Bin 0 -> 863 bytes .../wasmq/envelopes/fault-fuel.wai | Bin 860 -> 858 bytes .../wasmq/envelopes/fault-in-init.wai | Bin 0 -> 854 bytes .../wasmq/envelopes/fault-in-prior.wai | Bin 0 -> 1019 bytes .../wasmq/envelopes/fault-in-seed.wai | Bin 0 -> 861 bytes .../envelopes/fault-indirect-call-null.wai | Bin 0 -> 864 bytes .../envelopes/fault-indirect-call-type.wai | Bin 0 -> 886 bytes .../envelopes/fault-integer-overflow.wai | Bin 0 -> 851 bytes .../envelopes/fault-invalid-conversion.wai | Bin 0 -> 847 bytes .../wasmq/envelopes/fault-list-length.wai | Bin 0 -> 917 bytes .../wasmq/envelopes/fault-list-misaligned.wai | Bin 0 -> 925 bytes .../wasmq/envelopes/fault-list-outside.wai | Bin 0 -> 909 bytes .../envelopes/fault-list-past-its-bits.wai | Bin 0 -> 918 bytes .../envelopes/fault-memory-out-of-bounds.wai | Bin 0 -> 850 bytes .../wasmq/envelopes/fault-stack.wai | Bin 867 -> 865 bytes .../envelopes/fault-table-out-of-bounds.wai | Bin 0 -> 864 bytes .../wasmq/envelopes/fault-trap.wai | Bin 849 -> 847 bytes .../wasmq/envelopes/fault-unreachable.wai | Bin 0 -> 838 bytes .../wasmq/envelopes/gross-decode.wai | Bin 55460 -> 55458 bytes .../wasmq/envelopes/grow-past-maximum.wai | Bin 860 -> 858 bytes .../wasmq/envelopes/list-echo.wai | Bin 910 -> 908 bytes .../wasmq/envelopes/list-post.wai | Bin 0 -> 948 bytes .../wasmq/envelopes/measures-nothing.wai | Bin 0 -> 816 bytes .../wasmq/envelopes/nan-canonical.wai | Bin 857 -> 855 bytes .../wasmq/envelopes/reset-draws.wai | Bin 877 -> 875 bytes .../wasmq/envelopes/signed-argument.wai | Bin 0 -> 833 bytes .../envelopes/surface3-decode64.job.json | 14 +- .../wasmq/envelopes/surface3-decode64.wai | Bin 41454 -> 41452 bytes .../wasmq/envelopes/surface5-decode64-job.wai | Bin 43836 -> 43834 bytes .../wasmq/envelopes/two-modules-in-order.wai | Bin 0 -> 1102 bytes .../wasmq/refuse/byte-after-the-frames.wai | Bin 41455 -> 41453 bytes .../wasmq/refuse/callout-of-another-type.wai | Bin 841 -> 839 bytes .../wasmq/refuse/callouts-out-of-order.wai | Bin 928 -> 926 bytes .../wasmq/refuse/contract-extra-member.wai | Bin 41464 -> 41462 bytes .../wasmq/refuse/contract-not-canonical.wai | Bin 41455 -> 41453 bytes .../wasmq/refuse/contract-run-changed.wai | Bin 41454 -> 41452 bytes .../gate-outside-the-clifford-gates.wai | Bin 849 -> 847 bytes .../wasmq/refuse/instances-of-no-kind.wai | Bin 844 -> 842 bytes .../wasmq/refuse/manifest-changed.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/memory-not-exported.wai | Bin 901 -> 899 bytes .../memory-past-a-run-s-among-its-modules.wai | Bin 1119 -> 1117 bytes .../wasmq/refuse/module-hash-mismatch.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/no-shots.wai | Bin 841 -> 839 bytes .../wasmq/refuse/other-capability.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/payload-of-another-kind.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/prior-hash-mismatch.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/program-calls-nothing.wai | Bin 846 -> 844 bytes .../wasmq/refuse/program-hash-mismatch.wai | Bin 41454 -> 41452 bytes .../wasmq/refuse/program-past-its-steps.wai | Bin 871 -> 881 bytes .../refuse/results-past-what-a-host-holds.wai | Bin 863 -> 861 bytes .../refuse/seed-a-double-cannot-hold.wai | Bin 871 -> 869 bytes .../wasmq/refuse/trailing-byte.wai | Bin 41455 -> 41453 bytes .../wasmq/refuse/truncated.wai | Bin 41453 -> 41451 bytes .../refuse/two-callouts-for-one-extern.wai | Bin 880 -> 878 bytes .../refuse/work-past-what-a-run-may-ask.wai | Bin 862 -> 860 bytes wai/quantum-conformance/wasmq/vectors.json | 520 +++++++++++++++--- wai/wai-quantum-cli/Cargo.lock | 1 + wai/wai-quantum-cli/docs/llms-full.txt | 9 +- wai/wai-quantum-cli/docs/local-mcp.json | 46 +- .../schemas/local-tools.v1.json | 44 +- wai/wai-quantum-cli/src/main.rs | 38 +- wai/wai-quantum-cli/src/mcp.rs | 39 ++ wai/wai-quantum-cli/src/wasmq.rs | 104 +++- wai/wai-quantum-cli/tests/wasmq.rs | 95 ++++ wai/wai-quantum/docs/qasm-diagnostics.md | 3 +- wai/wai-quantum/docs/qasm-feature-matrix.md | 1 + wai/wai-quantum/src/qasm.rs | 37 +- wai/wai-quantum/src/qasm/diag.rs | 3 +- wai/wai-quantum/src/qasm/elab.rs | 116 +++- wai/wai-quantum/src/qasm/parse.rs | 29 +- wai/wai-quantum/src/quantum_tableau.rs | 114 +++- wai/wai-wasmq/Cargo.lock | 1 + wai/wai-wasmq/Cargo.toml | 2 + wai/wai-wasmq/README.md | 7 +- wai/wai-wasmq/mutations.py | 164 ++++++ wai/wai-wasmq/src/abi.rs | 6 + wai/wai-wasmq/src/engine.rs | 26 +- wai/wai-wasmq/src/envelope.rs | 77 ++- wai/wai-wasmq/src/interp.rs | 6 +- wai/wai-wasmq/src/meter.rs | 84 ++- wai/wai-wasmq/src/profile.rs | 27 +- wai/wai-wasmq/src/run.rs | 348 +++++++++++- wai/wai-wasmq/src/vectors.rs | 3 +- wai/wai-wasmq/tests/engines.rs | 229 ++++++-- wai/wai-wasmq/tests/reading.rs | 109 ++++ wai/wai-wasmq/tests/review.rs | 73 ++- wai/wai-wasmq/tests/sweep.rs | 434 +++++++++++++-- wai/wai-wasmq/tests/vectors.rs | 115 +++- 95 files changed, 2730 insertions(+), 405 deletions(-) create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-fuel-instance.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-fuel-run.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-in-init.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-in-prior.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-in-seed.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-null.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-indirect-call-type.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-integer-overflow.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-invalid-conversion.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-list-length.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-list-misaligned.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-list-outside.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-list-past-its-bits.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-memory-out-of-bounds.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-table-out-of-bounds.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/fault-unreachable.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/list-post.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/measures-nothing.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/signed-argument.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/two-modules-in-order.wai create mode 100644 wai/wai-wasmq/mutations.py create mode 100644 wai/wai-wasmq/tests/reading.rs diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index 157fdad7f..5267f52a4 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -14,14 +14,22 @@ 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 fuel does not count.** A reader bounds from the bundle alone what a run asks of its - host outside its modules: the memory among its modules, its results, an envelope's size and - the work of its steps. So an envelope that opens on one host opens and finishes on every - host, a 32-bit one among them, and no small envelope ties a host up unmetered. +- **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 (`Q209`) and its + bounds for programs from others (`Q323`) hold for every reading of a program. +- **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. The stack is - bounded by weight: each function's frame weighs a base and its parameters, results, locals - and bytes, and the frames suspended at calls may weigh 2^23 together, so the bound holds for + 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 @@ -40,9 +48,12 @@ 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`), and `verify JOB --bundle ENVELOPE.wai`, which names - the signer and how the run ended, holds the body and the result to the form the text gives - under whichever key, and runs the envelope again to compare byte for byte. A bundle goes to no provider: the provider protocol + (`--engine`, `--transcript`, `--replay`, each record written and read a call at a time), and + `verify JOB --bundle ENVELOPE.wai`, which reads the job's text strictly, names the signer and + how the run ended, holds the contract, the result and the receipt to the forms the text gives + under whichever key, and runs the envelope again to compare byte for byte; the MCP server's + `verify_quantum_result` gives the same verdict. 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. - **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 diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index c5a33026d..3f06bc88d 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -103,7 +103,9 @@ its length. A `name` is for people. `method` is `stabilizer`, the one method of 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`, each budget and the size of each input are at most 2^53 − 1, so that every JSON reader holds -them exactly. Every module is named by at least one callout. +them exactly, and the budgets are within what a run may be given (§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. ### 2.3 Reading an envelope @@ -117,10 +119,10 @@ A reader MUST refuse an envelope, with the code of §3.3, when: 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), or the modules hold more than 4096 pages - among them (`W003`, §4.7); -- the program is refused by the reader, calls nothing, has a gate outside §5.1's, - declares more than 32 768 qubits, or takes more than 2^21 steps (`W016`, §4.8); +- 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`); @@ -165,7 +167,7 @@ own block. And: | 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 4 MiB of bytes; 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^17 bytes. | They bound what a reader, an engine and a frame need (§4.3), with room for what preparing a module adds (§4.4). | +| 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. | @@ -178,8 +180,10 @@ vector instruction is taken, the table never grows, the memory grows only within 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 -prepares a module at all of them at once (`wai-wasmq/src/meter.rs`). +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 @@ -194,13 +198,13 @@ prepares a module at all of them at once (`wai-wasmq/src/meter.rs`). | `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 | +| `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 the reader refuses, that calls nothing, with a gate outside §5.1's, of more than 32 768 qubits or of more than 2^21 steps | +| `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 @@ -230,8 +234,9 @@ module as written, not on any engine: 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, the page count it takes is - charged, whether or not the growth succeeds. + 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 @@ -247,11 +252,11 @@ its limit less what it left, and is taken from what is left of the run's; a call 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 one for a page of `memory.grow`. A +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 a -`memory.grow` that fails, the profile charges the pages asked for and that engine's own -meter does not (`wai-wasmq/tests/engines.rs`). +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. @@ -262,11 +267,13 @@ Calls are bounded by weight, not by count, so that the bound holds for a frame o size on any engine. The **weight of a function's frame** is ```text -576 + its parameters + its results + its locals + the bytes of its entry in the code section +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, and its -entry's bytes the value of the entry's own size field. The **stack's weight** is zero +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** @@ -278,13 +285,13 @@ So the frames suspended at calls never weigh more than 2^23 together, and one mo 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 their bytes allow (thousands of values held across a call, a new value held at -every step of the largest body, 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. +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 60. +body the profile takes, some 490. ### 4.4 The prepared module @@ -297,8 +304,8 @@ reference interpreter does). The reference's native engine does not, so it runs 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 the page count, each taken from the operand with `local.tee` of one - added `i32` local; + `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; @@ -331,11 +338,12 @@ 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 each call run on a stack of the - size §4.3 bounds; + 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 runs the prepared module too, with its own metering off. + 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. @@ -351,45 +359,70 @@ The modules of a bundle hold at most 4096 pages among them, by the maxima they d (`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 fuel does not count +### 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: the program's steps on the tableau, a fresh instance's memory, the result. -So that every host can finish what it opens, a reader bounds them from the bundle alone, -before anything runs, and refuses past them (`W017`); nothing here differs by engine and -nothing here is in a receipt. +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, and nothing here is in a receipt. -**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. +**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`): -**Memory.** A program takes at most 2^21 steps, its loops unrolled (`W016`), since a reader -holds each of them for the run. A run's shots times the bits of a shot are at most 2^28 -(`W017`): the bits of a shot are the program's, each of them a character of an outcome in -`counts`. With the most memory (§4.7), the largest envelope (§2.3) and the most qubits, 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. +| bound | | +|---|---| +| its text | 1 MiB (a reader holds some eighty bytes of tokens for each) | +| nesting of statements, operators and brackets | 128 (`Q209`): a reader's stack | +| 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 of a shot (all the program's bits, its blocks' +own among them) 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: +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, those the reader adds to a program that measures nothing among them | `n`·`r` | -| each callout | 2^16 + ⌈`b`/64⌉, for `b` the bits of its arguments and result | +| 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`, `wasmq-prior` and `init` | 2^16 | +| 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 | -The 2^16 is the host's part of a call: on the reference's native engine, a thread of the -call's own. 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 work (§4.2). Measured at a 64th of the limit, the -reference would take at most about two hours at it. +| 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 @@ -470,9 +503,11 @@ run with `bad_result`, with the fuel used up to that step. ### 5.3 Lifecycle -When an instance is made (each shot when `state` is `shot`, once when it is `job`), the -host calls, in this order and each if the module exports it, each under the limit -`per_instance`: +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); @@ -505,7 +540,8 @@ the same way, without writing a bit, and applies `x` to it when the outcome is 1 reset of a qubit whose outcome is determined takes nothing from the stream. A shot's outcome is its program bits, bit `n − 1` first, and the counts sum the shots -that read each outcome. +that read each outcome. A program that measures nothing has as its outcome the bits its +calls write: no qubit is read for it. ### 6.2 Faults @@ -567,19 +603,26 @@ the receipt's digest is the same everywhere, and under one key the whole job is. ### 6.5 Verifying -`wai-quantum verify JOB.json` checks, and exits 0 only when all hold: +`wai-quantum verify JOB.json` reads the job from its text strictly: a member twice, or a +number that is not a whole number of at most 2^53 − 1 in size written as one, is refused, +since readers differ on such text. It then checks, and exits 0 only when all hold: -- the receipt's signature, by the engine's own check of a job receipt, and it names the +- the receipt's seal: a receipt of Ed25519 over the RFC 8785 form, its key, digest and + signature in lowercase hexadecimal, that seals this request and this result; it names the **signer**, the key the receipt carries, as a `did:key`. A receipt shows that this key sealed this request and this result; it does not say who holds the key, which is the reader's to compare with the signer it expects; - that the body is one of this profile: its four members and no other, `status` and `method` as the signed result gives them, and no whole number past 2^53 − 1; +- that the contract is one §2.2 gives: its members and no other, each of its form and + range, the method this version has, every module named by a callout; - that the request and the result are ones this profile writes, under whichever key: exactly the members §6.4 and §6.2 give, each of its form; the format, the profile and the unmetered energy §6.4 gives; the method, shots, seed, budgets and modules the contract gives, and no callout it does not name; fuel within its budgets that is what the - instances and the calls add up to, and callouts that are the calls the fuel counts; a run + instances and the calls add up to, no more for the instances than the instances made + could be given, no more for a callout than its calls of its largest, and a fault's no + more than its phase is given; callouts that are the calls the fuel counts; a run that is `ok` only after all its shots, or a fault in the shot after those that finished, of a reason, a phase and a budget §6.2 names; and counts of whole outcomes of one width, in bits, that sum to the shots run. @@ -589,7 +632,10 @@ of this form. Only running the envelope again shows that a result is the one it It prints how the run ended. With `--bundle ENVELOPE.wai` it also checks that the envelope is the one the receipt binds, and runs it again here: the result must be the -same, byte for byte. +same, byte for byte. A wasmQ job carries no history, so `--history` is refused for one. +The local MCP server's `verify_quantum_result` gives the same verdict. A verifier of the +engine's job receipts in general (the job service's, a view's) checks the seal alone, +which is the receipt's own claim, and not this section. ## 7. On hardware @@ -654,8 +700,11 @@ feasible fills the target, and atoms are moved, never made or lost. - `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 each kind of fault, a callout of - the list form, and a program whose resets and measurements pin §6.1's stream; + 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 measures nothing; 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 @@ -664,7 +713,8 @@ feasible fills the target, and atoms are moved, never made or lost. - `modules/`: the modules of record (§8). `wasmq_vectors verify wai/quantum-conformance/wasmq` checks them on every engine of the -build and prints a digest of what it computed, the same on every host that conforms. +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 @@ -676,6 +726,8 @@ 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; + 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/quantum-conformance/wasmq/README.md b/wai/quantum-conformance/wasmq/README.md index cff96288b..a47b4e963 100644 --- a/wai/quantum-conformance/wasmq/README.md +++ b/wai/quantum-conformance/wasmq/README.md @@ -23,5 +23,5 @@ 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 \ - target/wasm32-wasip2/release/wasmq_vectors.wasm verify /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 index 794e1e44ab911117fb6e0e4a27f98ea01098db4f..c19d3a20cb4ba5cc3c5490ded55c120f72d34684 100644 GIT binary patch delta 38 ucmbPojB(O2#tG**9g-Lr!UKH`{Wo6cX5un8G`F-cvoy3Y+5C=4Edc=jQw$dX delta 40 ncmbPqjB&~_#tG**ost+B!UKH`12!l03 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 0000000000000000000000000000000000000000..10e93d5be809785522a8c1c364c9d89ebe843777 GIT binary patch literal 852 zcmZWnO>fjN5cO^=RiqWaA@Yez%M#~f*Vz?_9yo9)P;uy?s#@EbY)um<>vhzYD82E^ zIPkX^@0KD&F7dp1o_T&V=O-W8PeMpf?}Vlc%BB&W&>9LYFp}B``+$Hp4va&@LR)Di z9C%h!Ky_td-DwN;Hs4bvS|N3%-2<|*=5*Z&$70^XATy7tP!S0Hz#4RMqid8vFGoH=aoF^HN z6P3(Br4m3f#iJ5Zrt)HzbBJ;|FC`?iEStq_9%p%+qzpUC2j{D1sMd%YyU`la*6POk zW|zKqYnxJgCI6fD#@E37%8J_C@GLEXITT!FJQuP6Sqk83!XS>M7wy7Ne)yv|Gda0bREpTGZ3BgUbI~uvgwvbQ+8J4<#X{&Bh6% zP?>o{slNWX!9 zf)8VyZHo{RNu+q*yqS5Pnd5`^^k)zR1G(gytO%VYWS`U^HAhQm!|gQ!+Bh%{5esdl zOYXq48VS{zg>A1b)RTW8C2zUVm3G(2#+^krJ8$kz07$3f_~Stk93LIhU!lKF ziDjDJb4~^VS8Zdxe1!tw%7Hbv2Hv5sfUJ`7bh;24yAVc;wF zr@nS;8>PLHf1CE2PV8o5dF|V9FO9(xWL_@vf(r>kameS4LYfGXi&QNYWkp&l)GKL1 zl~^Q6l8QN}l+&Ei;*T_%qBd>XsbRy2UAH~9XdtSG%KuxiXTGJ_X-wiDl#G}*o#UoN z-FNt{<7*?xYLQ^38r*r~CyGqJ8Q;KS;chp!zb5JYULg->OUCizr*|KY4n7~oY?iDe zZOW>b5Mvg(L0&gQgsh|8G-LG=SK}l<-HTtnjd#a&H0=w5@pc{Uuvpxm>^eH19PyjD z2Hy1+ylK(IWP9r7rgpiKj2}4(uO%pny6(}Rnlg0}7fPk?ctVhH_Q<_3} z2&`H`dF5c+8VA)7?`R5@sgka@D;8I$=O76pSe6PhcLWlG2;mmE9F2Op}=lL@t=!fv!ev~i|(q1k2V z-MCttpydCdy=4Qt-Z)W(F+5A-kb%mJdA<}8Ak<)l(|fGrWs$Rg_c|>mM|^n zNs^>;CKwYe=WO{`8cT7e#x}#ix*n(QS{zYFbqi(qH{hV93KI)4u=w<-@15ldq>S zpC;?b*rIF|#8^dbkhhy6Lf6rLt+9TMH*%3*9>;Iq$9r=f4TpnZyj@3oESArfTSqrT zAbuBDAa<>T;5u}%*zr0dc)55}Wb|@;P001=arAg_FealHuf~VQ8jlf8Mo~0AAmr$f H#N_BVRmT9T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9259e6a0e845fead0f56f85442df5aa98c1a93df GIT binary patch literal 1019 zcmZWoOK%e~5Z+BvKq&_f{D75DRHVV)M|L+6NIh`iP@v+_LshkYj8oHnB+jnd5T)h6 z@y8f%N-Mw@dp^&5zBxVm#D006w;?y&kSSqfN=BpvWjIR0RNSr+FjWIpgNTJiVI*(B zY1Imf3k%EESSY*y4Uv4og(*z4Ms4&tUbehy+H!ouSLG6|G%pIl8E`5y&z-4{ELsWpvx z5=AVOa*_k&G>kb+AdVBRbEP7ls+dtJ6PajupH z3OlhqcSSC2t&J=B|LMGAT~_tn^3wJDu?G>PpfZ_cITs3q=8(k^gOCc5iBPASMR_Q- zh`3^uX^~J$LlJYvuniH*pR!^h4$oAJZZaDj8nG?TVncKbg}WDEFWl^0yLJatM$A;J zhF22v{)5xq$M;$wma;U@COOMubXh`kRXYx$iR0oAu$aLEQU2miH#5g(V)f8D9d_jMd|!LL;j8|hcl7hcJ}$`qzze*6ztSbL&U8ocq}IuS@caNtm&;?P4?m0j;_veaHX8(VF#+(+Q0 zIP*@-I;98^$;!;P-~4?uSLYw-PtWsua?dqc5xNM;oKzq+M^9+O?EwL8S}-jl7MfBQ zyamUqC6u=ocAd6Rjqg1vc*BJ*b$dWI=3MMLZrZL|-1B|4!zjheQt;xAtV~yy!<-bg zk-F8j(Pl$dJ-O3Hjm^9fluM&%3(3puZ+= zoC3?#o#R~h1Rk2&y7m<|01qu#^W@+RT?u3r4maaK=o~{t$?qUS7L$_6L`8YHOj0Vc zkmo!Tc@{&Orm|ppmZl;C#^Q{#5YmJfIi(_rIZLI=3#=#(t;?EZb7FXf)kuYCV|8s^ zu^U&rv9;2!lmA6~Lq~SKvAl9JJhv7>29gyC%ejys6bBYZ6qcb7Oe|HVXp}DtB_b|q zNR>#!FkFh5Q_3leX#Te~x*+|Z-WZ>zO&$5i!LTRUHcwhF5Z+k@uy!Wj~;%L$JuP^O~K&aOb}fdhvE6^9lJ%vP+g@AQO!0w~yyHUG+T8&gYA&`tH?D6MH+n4WOOX zt@X_=eDBV-O8ZIv7wrw5*zLyh#>?~`1+vVt9otE;0X^)nL$?(>^iUM7WYMWsTXqy1ZD+W@ zvk%#M|1`?!It>99l$2P6h&6X|GDcjyw&Wg|iKsxXHA0t98&d>NS z>YkHfo2qr3%Z|ZAU0EkTM*;BAfHlt!zM(CEE&cv#cn~_f6j_Kn2$2QAvpCLWqEra; zRH!uN^C*b~4@A6(WSj?(Ml#G5L^4u>$7v`+2>>#Q6O~{``PjIsX)t9*l-P}xh}KqD z)-}6wy=zSld6M&R5oEax zh2lQX<=prEK%#OkcpCD>UuitRnQBuH1M7O6x@~bp9m`uN+la{r$7j#vK^)aLKIiKY#h~ z>HPHTnHSFd)kK@DXcc%^MXiyyogua il$??yN+z#HBXV>|Jamrhnw{<$Egs#W#+x!NAWdhIu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2adcf2bba5281fcbf0bf3f0f1baf85304d5872e6 GIT binary patch literal 851 zcmZWnO>fjN5KW2zDO?c0Ao2;MWr_2#PId)SD{?>rK>_C@ly6 z8E5_z<7``m$R#sx-poAD%<=vQ`pfgYfn0J;mW0kDG9xvVnxiMQ;r1EJM7B;=MP*3rJNZxXxE8Sfq8*9!tJvUun&oBA5-e8vERVBE*AWPF%)i@)v zF{SQwW3*Y58nE|3Hoef@mMrrb*`7DfO3?y9IvvNKd!BcEbVz^u z?l}_MG`-`T3FcooXAW>1tbw;d9g@Trm`$cA;}`iqC&EabGg8d;<|HHQ#K=p*Vv8Jh_+TY z)-^kIy<6KT?UejKv^R8OH*3plH--mk2{0&gndJo+B?!eKPZJ7pBt$M^#S~47SSpe5 zl15aCEQ+F7q?}St^Mn?ErO_B?s!cl$Y#4Fsw#N|-MD?$~)0^!@R2EOerIMx^%JYI3{&Vl($NhJE IaC`gg57m_eHvj+t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e07a8375d1d1d1b6c85d07fda8b13ab4c95e1c13 GIT binary patch literal 847 zcmZWn&2H2%5Kf8!DIlI9@`*~z5+~VBva2Fhf&+&F6^99^MVh6Ymrf{tV_nrWnGnrt>_%!tTcaA| znyp;#)-+PNN&Xk@B^}w#+OXQma6eiC8I-x0uW+f5N6%V59OP(H^SInP&z;fq({-dqOb;UE}pm%$zj`MqV9 z!DbAEM_~Br1jAwH-eafs_LW4h1R>Jycb@k8!%xk7Q%34VKj& z_-SzEH!!ujrvURaSuZ-n6XX1uT%O=me z5fN1>RjsO0l?yU$$&D&y?`!Pwy=Hm=n7bvbhO2r_rm;UF>+8yhAxZ#fA9UUCfn}YY zp3pzGsYhU*sxda_ErGjbr49cK8Ngi)y4)#PM3V!Vdfi1|5h`03p0gVW(2Bw&4T3oE zrIg&~NlHUs&;W!FkxzxkMe0-OXL1sG!cVe^45B0yVc>aw9A>7Ud{djK)Qd)>WCf-p z1;R_MDs8e|nB+@cWy(JSLM= zq%ov4nPf5Nz$YxGVGL0i$EgJ7nXr%Cg20S<*>|k%uGblF-!6TG&0x?@SmC1X2sABr%bx; z%h!*ePv_rG++gg@994?Ek>Fw!xj?)17!fjawsUo}H@GF|v43&wzWw0tMwV0Ft#55|!Gp>t~z@q#QVKC{S_ep{m+ldp2F_S2niVU|B^7 z@yocvZ(`O-ix7KQ&wM+NZyr9Kzhgfw%WBCDS7b`qn36-1L#jAxLKR%E5l}@9MU9At zDpLus!B~|kWEUEijna_!?=4Aq#f8dLy+&({IbJrrsGEFz!&mtdy(G^v!INt;Et)Lr z4oOlLsj5|3sA55;ExA@j+WQ)Ne6P7)0OoGVvJt9Yk!j=}lGRmdL?0ypwD-F1*TAw) zPmbAd+tia_p0Y7EXDxx-s?>(RL)J$R4Cxj;$8;n| zxYDZBCfm75Ug=UQL*@Ury<)v}xzIc}jd#4T3m{ zBZ%1~kdY98yd&mE5c*LRr@*NRd(X}D%$TdbV{M0h*A4clC9;9cd@Ilwrl(j2-5jKh zm?~B^FB0T^kJHV==TaaRu_&H|NWsUDIn-CB87{h*CHVpx8Egmm&rEBxVrYj`CSCX8 zR_ZDw3>8!NDl$0_|2~M96HknX4mT;+C96?)iap^v2oBvr)f126mRS z(H3>YUs2CSSN)Uo+R1^h8VyF(sA9x}*~r2L#?7+qXM@X~J3RK?<-pv-;lQ$YcWgY5 zpAR49ke7Nru%0GZBmN#{4-<5gmOVNe>{|1$PxfBmz3*7zz?y&GcWm?hw$*<;=zD(v DhYSye literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..31ba83d26695308b4877abeac2e56aaa9dce62f2 GIT binary patch literal 909 zcmZWoy>8S%5VoBJkb^`jI*_#$k&a{iyWTs2bOi-PfQTYR6eVkWd|BeZ+q0D%mK6$~ zg!iDM7IPOrtfT%lDiiXsu|6`56C zQ4AB3)>S53Sy!@Jl37o#WR;D!nvd_TP)oqrJy~^$Y}aJQya`!f)>;fu0zke$4Bsrn zI5|FYf0(+S6ywyL=A8BfZkk%@{Bz6zZdy>)R>2~=0?3RGXJbXE+?gbWxB?$p9`j<( zBk)2R@F2=4Fium!L4*-^V=rU^^3dlor4)i7j{GPWlqXT@<`7dKBPN?xd*ysWDp6uI zQX<+YSt}jvOvi3iolBj`|6P0Gj_i7=M5+7mCp9;7^O#c_K@$5ucoZW?nacvt&%-PX zdCD1Jj$zJ{1d>oNH((G2jPVQv)o!<`xxTNb+>8zD2W-3Quthz|J1F$G0d=lhN@gzB zsSl=%n5@>Vs8Y=PH@Et=>Z`2-3x*v{{tk+yonkcUGi z!|?9?>$k^?4@ZtarE^>g3OGNluw}cIZ5RjlqE7NxRCD`sd~#koC5UyWK&uK>^msV84cuQ`EW>fy{eedj8hPk(E z;<^81=UxeAtu~hNFvS}2@6cfgM7*4tkrVNSw0Z`-vVp1t_oH}VMkb2<2VZg*;hiTeu$LUNHCv_8TC5py@ z13wIL(KHT*Xnj@MX9O=P!Jc&8w}IoF zo*d)vuB}Hvp0cqvXDyO9RcS2$3>e6pS{l6-pok`yXc~0qeMKPLx$vAVB>^jpsTd0) zQ%dndg{jpC*!OT-y|YAZao47&Ahw0`q@+1lCwBCKk}cw^tbPs*DsHS z{ngCdqCWrgG&Aq2SNSjeLb7#Zq!kT_5b1-NC7>pdClIrC5Z2an-C^c^~I=ARFJ*M7h3&wsd6 I$0`nf0*~Ae7ytkO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..14d8ceb2434b209373ea77ae31f17e1dcbdf855d GIT binary patch literal 850 zcmZWn!EV$*3{7YYBK?VIPE=Z!Op@Kq?h2$HIB+OXap<9{iYM`GhbEKkY@)VA>4iVx zS2*)8d;*?qDMI8D`}x^^WBcs*9sl8ZUe9hovnAuxh)r09g$DAZHel}v(56Mx5@N9_ zbq*~$R$X9uX>rqOi`5wKSq=?IUF!CZ*i>`6>AQMNt&X6gUSi62AB=jmI=oZJKdl{XkQ<9gV1Gs>9O#8?={hq%>(N;y#j0m^RxM zOisH0;17W>wInR#*+R@0T%=U8!uG0mtECioB45#xf`|S6v(nZb81ZPyY#2U#eEa_N z_|r+4OruqxOIy zT?JQTAbcHG2;0u0b1k`4>}VBubh&g>p8wSU?BDzMdry4tVBe?j*@5pX&kGLy=ic$J K@B2r4-r;Z1EC4_N literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai b/wai/quantum-conformance/wasmq/envelopes/fault-stack.wai index d7a0007c984fb55520d865715cc4bf6f2a99653f..8f2b03be5a6b8e49f68f076aba0185acea9be310 100644 GIT binary patch delta 35 rcmaFN_K7!GB-50v@o+Yv@n^R&GZ8R;J^#( delta 37 kcmaFJ_LyzLY0fE33=H9czJ}*FUg>7!F+c*7Gnsw>0Q7$h6951J 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 0000000000000000000000000000000000000000..91d14eb0733a03c0604e3187df8e349db61296d5 GIT binary patch literal 864 zcmZWn&2H2%5Vm0pB0cd2kxx`wmN?1gXICKgz=1=7ibD@oRqSNEHBFqXH&I)n^c{F6 z&b$-jY@tHrl9_M5nfW|3XU8A-PeRDRZiHql#uovbvl>b*(39E-dxwBF9heRg3vHzf z(Sc(%B~+Idw!OAcPw{~jq7_nCy1PR*)?93RVY*OmG0Jj}j^Wfkc`U==87%!#=p|eksmAHloS&=N1h%$)MEG!aL zF0&#^!z53`bNO96w#C<3kF>Q98 zFa_%VgFghmH4?Fur+Jp-Jd3ep4c%4aR*Na_M81PX1rPiCXQiDxFy!Hq**Jds{Ql$V z@s|@nS_ErPo1*GV@Ucp{LEdhG2wQvmwfgE6-pF}&apb>#=kLw6HysXw{&wx{F<<_* z?Ap7U0{&aS2C?fcIM&kO7H3Y(-NDgOi%d;6JwEGPjDF5vM literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/envelopes/fault-trap.wai b/wai/quantum-conformance/wasmq/envelopes/fault-trap.wai index d696919ebc676a3872d8ca2b72d48b7e60c0dbed..56508a134065f04099cca054562fa3bb10f36b32 100644 GIT binary patch delta 35 rcmcb}cAjm*8P0kp28QrJU&C`7uXZzXnH!p0T9{cHT9{1EVR{Mx+87I^ delta 37 jcmX@lc9Ctu8O}x~28QrJU&9L?QaXs 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 0000000000000000000000000000000000000000..4874640e50c212dea86c3bad7901fd474e524975 GIT binary patch literal 838 zcmZWnO>fjN5KUSYr0^#qpQu!pIL_`Sy8@{P4jc+p9D1m#;*asxG;y+CM{SAHKgXH> z#5h}u5V>UL&6}C$nYlRqNPk9AG>|)P$coTeMovfr%5e0AvD`f%V5|r05pmGfMsg2< z)hej39c+8!pqb(Wk-X!=)W$y`8*9$CJ-5DZW_NtoY%xpox)xmCl9lc2dORVrwaR$Y zT4Oh4HIQ3l)s!3f828+50DBK)+Y94&WL2J!-Ax;;0xbZf({a2zh@y-0Gx{qI&ym=s z?E~j>AaLKcF38s?0Pa0F`{dvo`Wnb88?UAZp|ejBHNORhEReZ^g5`|z1zjuzlf_)i zMKza7D^XT*PI+EXQ52kM30ao0WvMjJBnYNB!;a#?hpG(KTHatc(jeM7)4I^?D)jDL zt4&by|IpsjiQR4-Z^9U!r7>87D&@SaxKJQ8hq7Q4@=ShVWc=|ED}DHjF_=I&#gq=fAFWk z_f{abq|2&YRJ1IxWCQ-D4XeeJa3Vjzp@OIV{j*XZ4h(s?WHOGQzkK*~e){z+WwUIZ z7%S^uL5fw>26?*~B4nNH*P80rcq5nP)p7dfeY!W-$#ggfrrUM0$5QcZxpi_g1=4qE t1ANyz2(CjHiyg0%2rn0JDvDpk-{Qyk@$evyULM71fQiQd32gqrwCO4UGR+Q3 diff --git a/wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai b/wai/quantum-conformance/wasmq/envelopes/grow-past-maximum.wai index b20c6d31d6a72e857c7cc323659927e8b7c600f4..6e4a5806697456b86954e9c9698dcd81fc7eb9e3 100644 GIT binary patch delta 35 rcmcb^c8hJo8O|;y28QrJU&C`7uXZzXnH!p0T9{cHT9{1EVfp|7-a`w~ delta 37 jcmcb`c86`k8O|Oi28QrJU&9LzQxo@y)|$(_{975VE3oLeVj0Lx&D%4v7+|NmU5FL_ifa6g46m zs!YYA24htwkX>q+H%dd^zOQI3Dj`*->LprZ%;CHdMcw4XJF&>;=#`2rlOn#QNCs>KQ7mKMb7aC~aGc9t{h(~TLVQCmSP5?4=k)IQ!B1jnbMsDgS zz8}RAhlquK%B2M93yzE-@HmeW5RM7^$jvoluG)^R*4TI5V2@TbZ6Gt>3iPGvDVE`E z4^l=<6^mLFG4lR{JKKkwQX<9~kHP>cco;H=`noj3MHjOq-#{aS-2nfYX>C>v?QqI; zz5e{=%JJgncMLp?Vw@>yvI|s37G#FK*iV=?{J%S62n?dTaLdaS!orVr2hFY!LGNzftF e?+3PJz7GfeL(AH9KiJmPx}nx||M&&z_kRO402Jf^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..29bbff584c6594a944e0ec652dedab7cca7a625e GIT binary patch literal 816 zcmZWnO>fjN5KRgcDg24ZCn_zAon-T|E0B8Nz@b3Jp@(YK`eVE`O`NPZQCp()*Ky`I zG2Sg8L@u%4yqS4^GZ&{H*^eLyMsmjuSrRszRrk-P`b zY8BL14z`1F(Cp(Qk-X!=)TVzxHrAYP2X6bJncwkUv&AgU>soMmOO|%1>uE-0YnACu zYmMEI!TpJI6q@QL;oI$ zecHj3E=L0QUF*DljSAqt2WOuy_=uqfvZT{`|028`Qbf&fAw`zt83hn|k!6+Uv`ksK zSX6l;Xu+}+RHYbaIuTi_5~?(^na%}>N|Zw9;QbU2y|0=sX2kLayO9Rb&Y9NvW^3QO zbFDUB$^WjsWqWqJalG+9{E-HQrcx%UWC`}qQUQx3EoCl&a>U9a;T5f8i^jD_WustmA4ct~B8en-B8Lj_Os`*)_jUoj-&n8`GK{_^3|`RUiQIGxi~ zWUQRU8|3Zgh>%rum}{(GfjN5KW3uQQ${JK7q7d?0jceAQg!NhXNIc9#*U3kIB|FakAb-ZHdwo;*W9W zKQZ1dAVeTwBhy+0d0CPJt7vmT1(!8 zW3>wEOAFgUTWI$8Bayu0Lf5*#LpIi&ZwGGrp_$+CU9-ikir2N^@|rBnP}kFp$kr&` z>(*$qA&Ze*YoqqLj*s^}w;RCTBiRl@_dBv+GqSsC-6_Ho0Mgks*(1-pI6q@QefJ!R zZQ8+cRwIGiuC-2ng#zHV2WuVze8W%!S{e-H+T6jAeQh>;bk6j+wWxl}BZT;+*~ zp=5C`(k$bpsw$Q!Q9+ThGAk-tWvPf|OcNO;tl|ngio4!bO;D}j4R#|9qMg;PbE@4#NVk>aH3;X}!YX|wCOk*NDO zP7fDfT7g*3^0LTERwP)mf&Qv>tHl&&k*{D;!NdOkS!wSKhCEy{nWhgP-@ZRT{d5+_ zbGi()k@cV;#42ioyxklTvJ4Ju4Xfwq)m5>cg~Q?SB0PB=qHk7NvR(#z3$55X+%AI$ zFtp3ShOaR5B%Xy4r7xC&hh{|^dH%8g`RM-MzkhV(d&iG`zw*4`nICwe_vXp>(Es-I E7s?{~5dZ)H literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json index 2109b79c8..e1115be60 100644 --- a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json +++ b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.job.json @@ -34,7 +34,7 @@ "fuel": { "per_call": 10000000, "per_instance": 100000000, - "per_job": 1000000000000 + "per_job": 17179869184 }, "method": "stabilizer", "seed": 1234, @@ -42,7 +42,7 @@ "state": "shot" } }, - "envelope_blake3": "f442722b7b2bbf0d3674f186cd86137f2a63110f20db73a519993c90d5dc67a2" + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236" }, "result": { "format": "wai.quantum.wasmq.result/1", @@ -71,11 +71,11 @@ }, "callouts": 32, "fuel": { - "used": 803644, - "instances": 215296, + "used": 836380, + "instances": 248032, "budget": { "per_call": 10000000, - "per_job": 1000000000000, + "per_job": 17179869184, "per_instance": 100000000 }, "callouts": { @@ -103,8 +103,8 @@ "alg": "ed25519", "canon": "rfc8785", "key": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", - "digest": "3c5ed83161d26ddd654aefb76291c823a12ad1c7218dfe7f435c14ec91dbeab2", - "signature": "9d7699445906d02dba7c2c088fedf4b6fcec8b1db029307194b2f37ac0c97cfd9b1bde68b845dd9158490995d7f56983c161a95541410c3e2b1d62de55240c0b" + "digest": "df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1", + "signature": "96f9a03e1a8b88d68cd1d2498fa1a3589556c9f628922eab3dd0284322e41de3640edbf71561c7fd265801a81c1250db5c7788b40ada40f42c8e6a773e7aa008" } } } diff --git a/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai b/wai/quantum-conformance/wasmq/envelopes/surface3-decode64.wai index 96525914fcb2e373efaac7f7c861b37063ae5604..b993941d4e6936d7e5d3aafa635460453437dae6 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai b/wai/quantum-conformance/wasmq/envelopes/surface5-decode64-job.wai index 74188cb3facab3960e2e6595c185a46774ad15fc..e9584bd5f9b67ce9ca9a1c0eca803a52ced204d0 100644 GIT binary patch delta 38 ucmdmUjcM04rU~abGgdJ$ga`T>hHktr#KdK8Xl`j?W@%_)viU2M`w9RYi4ArD delta 40 ncmdmWjcLy{rU~abvsN)Mga`T>hHtzs#KdEO1U7$Wa$f-eGC~cW 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 0000000000000000000000000000000000000000..75830183ecacaf0f0572c9876f67ecf71c964df7 GIT binary patch literal 1102 zcma)5$!^;)5S5((MV)?xz*7RauBer=(E>U2&_mh=J>(Ds2`LU0HWFo15|Y$V`f>fS z4(+UH4%J1oyqS6AJf0kWp?^HjTa#<9$(+zxK&GSysX2N=8*XngpiKv+!-$1tr3>DH zLp2hra|^3pTc|hrH7WR#3tj2%7SY&qw(7a*`g(TF+j@mnidU83#TA*GzN&^PDHlmR}i^okqS8% zDgr5JCS?qiB~d5~m4-PLA{8u1ql^lcKw3};Gp-`ZvrKZ4M+#`_BR`r2i-|Tx)l2ZPi!2e>&M-n2ldbH0wT~J;WoHNe`%nJXe9V=v zmR{Tg`NTX3#L7Y7||t#aR@<>C7q;Y%OA9bb6f#prHy jx3e?$MsMDYceT+SW|TLYOvd<(_I5qr+xw4-@@2(e0Z>3O literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai b/wai/quantum-conformance/wasmq/refuse/byte-after-the-frames.wai index b234478b63e96773cfd915d038d8dd9509355970..fb996db4148d850352e11231c590bd7b65f9d49e 100644 GIT binary patch delta 38 ucmaEVnCb0drU@4~B^NRhHtzn$i!uCXl`j?W@%_)viTd+&v^hHx(*Bg delta 40 ncmaERnCbmtrU@4~r57?Vga`T>MsB<*$i!oS1U7$R`Z*5(GffUO diff --git a/wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai b/wai/quantum-conformance/wasmq/refuse/callout-of-another-type.wai index a7e8144cac322b4dfeda9e6e0a1adca6d84d863e..6ecdb57031e19953ea900b63596b8b31e5a5dc4b 100644 GIT binary patch delta 35 rcmX@fcARa(Y0e5J28QrJU&FH-uXHnVnH!p0T9{cHT9{1EX1WIe*5(U? delta 37 jcmX@kc9LzvY0fGp28QrJU&HeouXHo=7$AYknN0Tp>In<0 diff --git a/wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai b/wai/quantum-conformance/wasmq/refuse/callouts-out-of-order.wai index 5d6caea0e53a6dc2c5bf87a0ccd928850f6fbe0a..42408a72fdc6f29468a52b10fb00252e46cd24dd 100644 GIT binary patch delta 36 scmZ3$K97CEY0mRZ3=H9czJ^~mUfIORWo~G0X<=q*XkoH>5)&UI0OsHePyhe` delta 38 lcmbQozJPtgY0isG3=H9czJ}j6UfIORV}JxUPhjF>1OWQg3wQtk diff --git a/wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai b/wai/quantum-conformance/wasmq/refuse/contract-extra-member.wai index e4bdb6f2247621e9f270f615edd9723ce883de14..988b2c8f31f8ec4e2e23fcb58a4b85cb1b960567 100644 GIT binary patch delta 37 tcmexynCaVLrU@4~l@>BEga`T>CT_eb&BSGHXl`j?W@%_)vWfY}JOC3<4MYF{ delta 40 ncmex%nCZu1rU@4~RTeTZga`T>CU3kc&BSAX1UCO;`Y{gxIPDH> diff --git a/wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai b/wai/quantum-conformance/wasmq/refuse/contract-not-canonical.wai index 2a241320270edb579e14ec6e6f28d1538c80e77c..1875564b53e22d7909104f38655653096c92b752 100644 GIT binary patch delta 38 ucmaEVnCb0drU@4~B^NRMr^z(#KdK8Xl`j?W@%_)viUpHk9hzc;|>l0 delta 40 ncmaERnCbmtrU@4~r57?Vga`T>Ms2()#KdEO1U7$V`Y{gxGkOj- diff --git a/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai b/wai/quantum-conformance/wasmq/refuse/contract-run-changed.wai index 54501e5c3f79908c4575c3d78bfa922f2cc1c19e..a86a28bb1a1ef86e4e599d4a790137cefe4b598c 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` 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 index d3f07ad498cbbcab1f4c64a73d6d16af438aafae..a25007851f6138a39da72f6ca8cb3306ab6ddc31 100644 GIT binary patch delta 35 rcmcb}cAjm*Y0d^F28QrJU&FH-uXHnVnH!p0T9{cHT9{1EW_kty+3pLY delta 37 jcmX@lc9CtuY0f4l28QrJU&HeouXHo=7$AYknM}_B?LiC6 diff --git a/wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai b/wai/quantum-conformance/wasmq/refuse/instances-of-no-kind.wai index 9c831e30d62a1229b27bd568f3ae328ced8abc92..f1a605fa757f0ed0e96eec14ff99f116fffeb2e6 100644 GIT binary patch delta 35 rcmX@Zc8YDnY0hdU28QrJU&9L>wpWo diff --git a/wai/quantum-conformance/wasmq/refuse/manifest-changed.wai b/wai/quantum-conformance/wasmq/refuse/manifest-changed.wai index f5f3cae354ff9f9ecff7316d90c498e9486aa758..94efdf2b426925761271c00d22c21ba3e41ffbaa 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai b/wai/quantum-conformance/wasmq/refuse/memory-not-exported.wai index 547a836b9fda4863eed4a69370ed2f71f0407b48..1e2e0aca1032a6e0a05a086ddd29d0708b2695e9 100644 GIT binary patch delta 36 scmZo=Z)TrxhI2C$14DSAui@E^SGyUx%ni*gEzB$pElf7&FiA230M-!-mjD0& delta 38 kcmZo>Z)KlwhI13j0NI}kF#rGn delta 37 jcmcc1ai3$tY0e4E3=H9czJ_TVuP8C`7$AX7%(09B;o1sV diff --git a/wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai b/wai/quantum-conformance/wasmq/refuse/module-hash-mismatch.wai index ffcf82715e4fe23f8baa95c00fd96397016d9668..02b0e3f90fb7e5cd16ffe9f07a2d025c8679700f 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/refuse/no-shots.wai b/wai/quantum-conformance/wasmq/refuse/no-shots.wai index e4f010d5dd97df2959d1220d1cc7aed30da45754..0287463245d48a0de5f6b725da635c24f6ebde11 100644 GIT binary patch delta 35 rcmX@fcARa(Y0e5J28QrJU&FH-uXHnVnH!p0T9{cHT9{1EX1WIe*5(U? delta 37 jcmX@kc9LzvY0fGp28QrJU&HeouXHo=7$AYknN0Tp>In<0 diff --git a/wai/quantum-conformance/wasmq/refuse/other-capability.wai b/wai/quantum-conformance/wasmq/refuse/other-capability.wai index 9bf5873f70f17755222181514e689320098eeff4..17f9faf51f455474be603978f81b3fbc604b5ebb 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai b/wai/quantum-conformance/wasmq/refuse/payload-of-another-kind.wai index c96f442c7feb02184fd160ef60502cdc8a562224..8350850b00d4ea6e59a2b2de069dc0aeda7142fb 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>g>Sqm$i!uCXl`j?W@%_)viTd+k9hzcXbuSg delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MQ*$)$i!oS1U7$R`Y{gxGWQNK diff --git a/wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai b/wai/quantum-conformance/wasmq/refuse/prior-hash-mismatch.wai index 993407835e98979e2e6e775af788bdfeca5db601..4d9e4b59b7330c1e194dfdd07db9b25b25deb191 100644 GIT binary patch delta 38 ucmaENnCZ=7rU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai b/wai/quantum-conformance/wasmq/refuse/program-calls-nothing.wai index af1eed9c5480a6a5ef8989f246f551e40e54cc26..713410b9a40a615da915ffa77ceea06f04289845 100644 GIT binary patch delta 35 rcmX@dc7|=jY0g?E28QrJU&FH-uXHnVnH!p0T9{cHT9{1EW_k<&*vku- delta 37 jcmX@Zc8+brY0i2k28QrJU&HeouXHo=7$AYknM{uX>hHtzn$i!uCXl`j?W@%_)viTd+k9hzcQVs|J delta 40 ncmaEJnCabNrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Y{gxGT#m` diff --git a/wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai b/wai/quantum-conformance/wasmq/refuse/program-past-its-steps.wai index e0d9ef8f856653db55449b428a124c01b51484c2..de7125b8aed76918227a5fac77d19ad1565b5a95 100644 GIT binary patch delta 169 zcmaFP_K|JEY0gDV3=H9czJ_NfUU}(jl9rsBWME{FW}IYbmYSGsU|?=)V3A~LVQ6Wb zW|EYgW?`C=YLb`)RAXV5Xli1fYGP_+Y+;<5YMz#uY?-R0qm)!xl3J`}WoT|NnTsiu z%iPf1(!$Kr(86SLHq%CfA_ZH8G>s%pYp%5XB8Ae-yb^^>h0HvKXag%F150y5W7AlL UYK5ZI;?xp_!f1oo$qvlw0ANot2LJ#7 delta 159 zcmey!_MC0PY0eo;3=H9czJ})~UU})8VriJ1YG7)TWM*k;keZTaY-ny~mSSpVk&Y8b0ftx%+3tB|IVq&eA(Ssefy CnJs|; 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 index 950198f111824ee2b967c0b2d2f13912c3bf76ff..81395d12ffc693086155258d9556464bc8f46565 100644 GIT binary patch delta 128 zcmcc5c9(6!K~ZxZrQ+1o6eTOe+S(o_28QrJU&Heguf23IPBAyKFg7$YO*S$yHBB+K zNHjIJG_W*EF*h(WvPe!fwX{e!OfyU~NijA{v@kJEO-nX2GfOg0F*O3JG)$iSpD~Hc f+|b<8!pzdp!enwTlQyII}OHDRPv`jTOPfoHlGPSfcGcz(Xw6HXq{GTz2 Z#{daT&SuhPG@Cq~sfNkiY_ce`2LQ?HCeHu> 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 index 0f785fdf5b3d1ed1a53374c43c0dc8745539290e..c330117c4df844eee8ae785d0736eeae0ed75f72 100644 GIT binary patch delta 35 rcmaFP_LObHEzWi(28QrJU&A{aA9piynH!p0T9{cHT9{0(VtNYzhHtzn$i!uCXl`j?W@%_)viTd+&v^hHoem2C delta 40 ncmaERnCbmtrU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`Z*5(GcFD@ diff --git a/wai/quantum-conformance/wasmq/refuse/truncated.wai b/wai/quantum-conformance/wasmq/refuse/truncated.wai index 216de6049ad5fe0ee273dc409c87405665e77ee3..6bbd721357a0a00e9956b4fb4cf39215c3bd833e 100644 GIT binary patch delta 38 ucmaERnCbOlrU@4~B^EL;ga`T>hHtzn$i!uCXl`j?W@%_)viTd+_jv#v2Mz@Q delta 40 ncmaETnCb0drU@4~r4}+Uga`T>MsB<*$i!oS1U7$R`aTZ;GLQ}} 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 index c563902949708060109bd0d5a20c40f50cba4d8a..017377cd0227fd89038e1ef0a75bd40cf17d5157 100644 GIT binary patch delta 35 rcmeys_Kt1BY0mjf3=H9czJ^~mUfIORWo~G0X<=q*XkjvW64N~Z>z)j4 delta 37 kcmaFI_JM7}Y0iaA3=H9czJ}j6UfIORV}JxEPhh$S0034Dl>h($ 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 index 7db5dedc18db77d7514119654dec0e634b31b246..6506cb55d647ec36034c3d5db0a6c170adbf422f 100644 GIT binary patch delta 35 rcmcb|c86`kRnBH628QrJU&G5A?{zbBnH!p0T9{cHT9`~OWqJt!;4cg3 delta 37 jcmcb^c8_hsRnAr>28QrJU&E^#?{zcs7$AYk#Y`^&^Y9E0 diff --git a/wai/quantum-conformance/wasmq/vectors.json b/wai/quantum-conformance/wasmq/vectors.json index 89f5dd8c8..9d7a550cb 100644 --- a/wai/quantum-conformance/wasmq/vectors.json +++ b/wai/quantum-conformance/wasmq/vectors.json @@ -4,133 +4,133 @@ "runs": [ { "file": "envelopes/surface3-decode64.wai", - "envelope_blake3": "f442722b7b2bbf0d3674f186cd86137f2a63110f20db73a519993c90d5dc67a2", + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236", "expect": { "status": "ok", "fault": null, - "fuel_used": 803644, + "fuel_used": 836380, "callouts": 32, "transcript_blake3": "037069c26ac0beba900fefb241e021dec52c77356506cad8ff39d3240c090d82", - "result_blake3": "d722eb7861e6bce4008be7a456a57f97d488212dcab4adb947ff0ea593d81246", - "receipt_digest": "3c5ed83161d26ddd654aefb76291c823a12ad1c7218dfe7f435c14ec91dbeab2", - "job_blake3": "69d1bed8bff044ad35c6a2f08930d9d888f9a6a5b97e6a181be87f7089e1e4b7" + "result_blake3": "35a686ec1b71982c65d7519e24166353ad35e8114ca03cf0003d38a93f7b06eb", + "receipt_digest": "df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1", + "job_blake3": "80676447542d8436b8c26afa4ef13aedb526f13c02c117abf50c970cb589b241" } }, { "file": "envelopes/surface5-decode64-job.wai", - "envelope_blake3": "3f74b00339613f7a03ca546a311e23a4e75f96281ecb15cbe1baa484421935c2", + "envelope_blake3": "3691fd8cac6fffe58d46a496aeff248a40d654129fe8cd23bae3dd6d3cebf8f4", "expect": { "status": "ok", "fault": null, - "fuel_used": 764587, + "fuel_used": 765610, "callouts": 16, "transcript_blake3": "eb5252076ea8bb4c45ccec714a3cc64b89e833fe0a49c36a28881b63b1c3799e", - "result_blake3": "3b3f96b0c542a1e2d822eeba582eb5fb75acb8a8421d5d31b479390330754d0a", - "receipt_digest": "ca45d086d44caa1b05fbaaff8746048398e1de0ca79f5fb8c47ac56ef1d89f52", - "job_blake3": "edbd378e82d417093aca4ecf64f78e13b4080d56e72c4f90f7de78e1263231ac" + "result_blake3": "c38c0aa8b9f6432b7fe4776d8d58b270893f683901c53a73742a407004f92b66", + "receipt_digest": "5109fd0c11be13f637530e8861ec2d625a2985a9a3ad9340e0d00d36aea2346d", + "job_blake3": "dcf23b94abc66a4b0ab701c80fa8d0215bbf24e044261211153d330dcf921817" } }, { "file": "envelopes/gross-decode.wai", - "envelope_blake3": "f7d77db307d341f469ef3d365a1d1503e9b20c49d750bce4c9df56fc93e52743", + "envelope_blake3": "0e62b798fc22ee783fcdf318188ab697f50c567487f71b512a5c555a7dd46cda", "expect": { "status": "ok", "fault": null, - "fuel_used": 1017820, + "fuel_used": 1021912, "callouts": 4, "transcript_blake3": "29b7d81e2ebc5d2dac6413ab3c15a2da2b2dcac03f3c68633a10bde60e59d3c4", - "result_blake3": "cee95ea4907b3d597ed71eac72f01f4a767f45034b86a7f8e630e4bbba9e7ea0", - "receipt_digest": "dfe46f19e382123a58985a20b22bfabdff85ae925721425d0581a4a5596fd696", - "job_blake3": "6b302e2d08ceb5b53843b1550f57229023cad737dc36800dbf8101b99dab6489" + "result_blake3": "c193273cf7f0065b75c9a8baaf3c7e91f0f75b427b511a065c1d6ccd0bbded50", + "receipt_digest": "7d65a55b4c9839e807f4ee8a8dbc5ff79776967b3c9f828aa0efec8fa36fdb86", + "job_blake3": "085e75df38df048ba9198560191639d2f101329d0be38abebfed28a76009f09c" } }, { "file": "envelopes/atoms4x4-plan64.wai", - "envelope_blake3": "01c344f03ed95fc91145e592e6f664d79c8693e3b1f1fc67b05245b82e8c7ca6", + "envelope_blake3": "ef6d0807dc70169b792feec7d08bfe824ec104cebd56ae4df18e5520107ed479", "expect": { "status": "ok", "fault": null, - "fuel_used": 543607, + "fuel_used": 576343, "callouts": 32, "transcript_blake3": "927caf8a41a7e9a654471532af1f428ddc86cb5efab5b0d64ba21fae2726d752", - "result_blake3": "f38bb5a31c8c84a015fd35eb44c1b610f69fdacf2349140afed0157f91163edc", - "receipt_digest": "0f75aaec80cbd1c79880c8d11665ca93b55c924902da221a60f3d948f9175eab", - "job_blake3": "b6217bc1f315c45d17f1af610ff5ce2fbf128eb2eb4bd7d54f57fb68bba4e9b8" + "result_blake3": "5d688283421aac7bdb96350844120919d636aa481b6b662392ee132b471c0fe9", + "receipt_digest": "51e4e50d256f357866845d7678ed836a39b57e0faf141dd458cd2c69d91f4c47", + "job_blake3": "10e89d19dc3b037fc32cb04fdfd928b7b36bb58784182e404223ec8d16516105" } }, { "file": "envelopes/atoms9x9-plan.wai", - "envelope_blake3": "c305f88b785110e9313ab6385cbbf2f03d8cf9596ff164f8a7c9bd5f16da4f32", + "envelope_blake3": "43647ef1419dcb7c0e584daa4201f8745f617141d14df8d668f75b35bb39cd44", "expect": { "status": "ok", "fault": null, - "fuel_used": 908639, + "fuel_used": 914777, "callouts": 6, "transcript_blake3": "e280bbc8ba046e7f003d173c4c280050d63e1b972ec216c465ce67bed621ed6a", - "result_blake3": "7b07a423cc3990f89d8840881e743591596e42286a9414205da1d7f19d84cb40", - "receipt_digest": "21372d7fbfe01ddefbcfda39b82a080950364f866d5678f67c969748c263e310", - "job_blake3": "77a787bedaeedbe8cd1bd6be79567cec76866342a291caa185c18d979878a0b5" + "result_blake3": "bbcdd6333d35a7a59de45c970f39d11a7c08fc32df3abe4fdd211008b4d0e51f", + "receipt_digest": "e22f0fca1ff06a0135cf12607bda14a374bf45b393cd563e0795e82c1a41c554", + "job_blake3": "452b81e0d98b2fd78f4fd4852033d1c5b6112f6994001de5e09c178a36dba636" } }, { "file": "envelopes/list-echo.wai", - "envelope_blake3": "620e356307de65962a480b1b39b1d544776256b4fa93409b20ec69fe6a12b5e2", + "envelope_blake3": "7ae91afc65551149a86ab445d24a28acaf52323f5aaa858d1859b0434ae156bc", "expect": { "status": "ok", "fault": null, "fuel_used": 224, "callouts": 8, "transcript_blake3": "3c53158450b92c3e6f15f7b735d7ef722b6c602d80c8dfd323f3d4f59408f710", - "result_blake3": "57b32fde49c9d54fb1b05c62e525e8d6e570038eb8b3392814bae61f150cc7b8", - "receipt_digest": "d16ccb1f136c266775f34a91595a6070c67aeb8bd584fd973cd5f2fc5acec76f", - "job_blake3": "6294494e4e6d4dcebb2fea11faf4d7f01fad81e251160024c5cea8629bc75114" + "result_blake3": "b934991a387b3cdb81dafe6c14bdbef57999f3954990b4089253796b605debe1", + "receipt_digest": "541548abbfe6224fe2523fb2f6133bb61e808e78f2757e635fdda8a395a4c425", + "job_blake3": "07eeda7a1acef2897e94c81389e009ef24d4c43c04153ea786ba30d1fc1c40c9" } }, { "file": "envelopes/reset-draws.wai", - "envelope_blake3": "bc9ccf4560a104210665a6f5451dd10157b99a0ee453e8d67088291f38e4d001", + "envelope_blake3": "8404b85c8109a980a8a8c21a56be5664f193fca55869a53e51fee4d2e466ea70", "expect": { "status": "ok", "fault": null, "fuel_used": 128, "callouts": 64, "transcript_blake3": "6030f402b046fc2ccce71fb0342fc46dd6da2347fabbceda3677815fa82472ed", - "result_blake3": "2c50ab70de4e349542311586325625434c23d0ff9aed52168789abb9495560d5", - "receipt_digest": "f6989072bd0f79091b82532c0608c6117e33d3e4f0b09dc4ad095917ff880195", - "job_blake3": "9f3c6394a5c9e5f3ccd323a89531e6901546a2b32958fa3a2325b5878d303bb2" + "result_blake3": "5780a171a07be0f783e2eacd8dc71a1a64d3b32c764f2d25adc148edd9c5ae50", + "receipt_digest": "5feb7d5ad3bdafb62a9c88bc4ea50a671a03a0d0e7375fbdabf5fb7260ac3f6b", + "job_blake3": "6dd8e6c35b99ca5ccff31681d313372f74f60dda2dbb88536c38b7ff31222d12" } }, { "file": "envelopes/nan-canonical.wai", - "envelope_blake3": "bccef76fd3eb96f0ade40cd19c490414b1f86c0f1a2f125247e5045a73c6a54c", + "envelope_blake3": "70571f247aeb32182ffa65b93404373656f13af4a53d6bd8338b471b2e0d7a31", "expect": { "status": "ok", "fault": null, "fuel_used": 64, "callouts": 8, "transcript_blake3": "d062b788107d413845ef253a9d9265548346393a28faeacfe3ef3be471ebfe7b", - "result_blake3": "2a27eb1a4a1d0b72c78a79061d90952d71c856677f1e8c6a710f5cc60584fa64", - "receipt_digest": "24b1cb568d741de313eff1bff6ef8b361022c9432bc948182b638c8128de9656", - "job_blake3": "93a087f52a8fc9fa2fca9b686ce2cd6c9edda589b564464b41cc4ca457b1d8dd" + "result_blake3": "2b79f2f7e4aa970c1e00dab10a32542337e551a8851c2d6f230b636cb22bd40a", + "receipt_digest": "5a3305371c79689531a7cbaab3187ae33bfdb522153537c16c8099c57ae0570a", + "job_blake3": "2e76d20a452820058c9bdb73069ca6238ab97a4e33c7c5a88096e99c4e574715" } }, { "file": "envelopes/grow-past-maximum.wai", - "envelope_blake3": "bd7614a64c8b5c25fa86b0c56ef10c62b22edf60bf8c1ddb94eb68d7ca5a18fd", + "envelope_blake3": "221f7185865476a51e4b5b1a483b2ab78392b40a5efc2c4a86dd064623c284cb", "expect": { "status": "ok", "fault": null, - "fuel_used": 217, + "fuel_used": 74896, "callouts": 16, - "transcript_blake3": "9a6988078f783b1cda95640067c315d4747c4bd4c33709c0afe72b9afe8e03d0", - "result_blake3": "54ccd6f7340b9acc7339e9a7e0df4c79671d79e70da79de6b1375f6f7c5120c9", - "receipt_digest": "d9029d82eb8eabb804c32da5162a82520fbc86f4e517b7be177545c0788ce6cc", - "job_blake3": "f94367d6b3904b86a275c90ec6fc373e72f9e64b9b721898fe4222f793d4e77a" + "transcript_blake3": "0fee265d944e8a9d3b6076a0e1c50647847446a3ef01adcfc61b014768cb838a", + "result_blake3": "f0d60769b88b7b1916de9c31159015e01d9b628fbe4dde3558d2c022f0742007", + "receipt_digest": "c7b6269b9a67c3bc4b55e2f9a55856e52edcb8630a2daa89d5fbe430d33bd90c", + "job_blake3": "1c38d94473fcc2c9093db03355eb656e0db5311cc72fc31d11b690fa8625da00" } }, { "file": "envelopes/fault-fuel.wai", - "envelope_blake3": "b1cb5f210f7e365281d534ea91b371fdbb497dcd9f8cdce927a6e4a7a998b7cc", + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35", "expect": { "status": "fault", "fault": { @@ -145,14 +145,14 @@ "fuel_used": 400, "callouts": 1, "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", - "result_blake3": "9e4928ef3f92fafcff7742a9799a680b53553b57607b82a306c338d8693b661a", - "receipt_digest": "f14551e8db32db811ee484ff7362563e2301dcbed0a707795f7eccd4772e9695", - "job_blake3": "e3588a1797e3176e181c970877f3f3dca336eb6707298ee044133777eacb0a75" + "result_blake3": "0750cc44f1ec6c1f133f31cd2b63e5e5517b85351294eaf23d68d81ee0f02e86", + "receipt_digest": "9cab411ba32a647e5c4b0ce53609a4308c1a07bd26c817fca582e3e04258bff4", + "job_blake3": "28ea3f814025c3ac778a726cb4465ca6e24d041515aab9fcebf573766d3bbbb1" } }, { "file": "envelopes/fault-stack.wai", - "envelope_blake3": "b1e71eb5c12fe22abf72d917bf9e62985fcb2d6d77621fc05831a334e51b356e", + "envelope_blake3": "760fcb114402553db1314cb63c50ff40ea3012aaa3620b0079e1b9684472cb5f", "expect": { "status": "fault", "fault": { @@ -160,20 +160,20 @@ "shot": 0, "phase": "call", "module": "m", - "fuel": 71453, + "fuel": 71213, "callout": "f" }, - "fuel_used": 71453, + "fuel_used": 71213, "callouts": 1, - "transcript_blake3": "730e9663eaf2424bb43900457124297f166ce516f2b7a4ecced29d9d07e6e7d2", - "result_blake3": "46b2a88f80696c055f3a7e0246c24a9b4a5714ba2c48515a87e56c2c46150137", - "receipt_digest": "99e998400ade53971b1bc1b214877d3b7e9812d17777ec88c68d90cdbb82eaa8", - "job_blake3": "033743289a2dd0c999bb2ba10ae8a5d5583c49d90d662df7628977aeab515399" + "transcript_blake3": "312cb35563349cf18ce5206254a933ba0e98ac08d15519719347b39126997727", + "result_blake3": "d65de2d3cadb96fe4e258057ed230b2e4c9f9ec5c44ae6eea6b08720d3c7d386", + "receipt_digest": "6aff4dc5a0cb49865e2234ef66f484b3f5844d3fbffcef5ef3ad420e452d7af1", + "job_blake3": "e039a7f6a78a1e16c10cb84fb1dde963140f2ae83d43425ad54af44904dd4ea2" } }, { "file": "envelopes/fault-trap.wai", - "envelope_blake3": "4b58cda2cfe8df0f5908803bc9ead00143dc1f7e59313d5aaf1f21faf9d70f00", + "envelope_blake3": "8435227e277d8f3fdea2c7847be85aa618389ec9d7432d72d0170f940da7fa66", "expect": { "status": "fault", "fault": { @@ -187,14 +187,14 @@ "fuel_used": 6, "callouts": 1, "transcript_blake3": "7d4291653436ebc69debfc0881d22799cd6d1e972c2cc81d26d62aa3b8bdb12b", - "result_blake3": "8fda85c034e24618c911a8222b384999d0be06cd9519ceccf73b3dbe078f96cc", - "receipt_digest": "17603ef29c635d8bc0d6e4de9ae4d25a41a8ef8a0ab2af9e607823acc01c0e1e", - "job_blake3": "3c82c705c84fae305099f5d858dd58fcdc369eb9d8e4ffa171411c968fbf392b" + "result_blake3": "87e38f767b2cf5d2648be7626b3e0c3a6fbd94a0020827b2894e6cd650406bd3", + "receipt_digest": "baaa13476d446f10eb9adcde00289198f713e0bdd02f886eb8ecfff56087caf4", + "job_blake3": "57ab50773e9e77dc909d3cd19d19e87ebde2020ba8abcabcc68eb3c27902843b" } }, { "file": "envelopes/fault-bad-result.wai", - "envelope_blake3": "dae1baa148c000230ef187522fa4d0770aa4c73ad20acf660a8c32a2eb61ec01", + "envelope_blake3": "91b61abdd43a815f26055fe5834b2cecc57ebe5951c8a41b55350f5ab6acba4d", "expect": { "status": "fault", "fault": { @@ -208,9 +208,405 @@ "fuel_used": 4, "callouts": 1, "transcript_blake3": "f64338e17fa3fdc023f35081e59fb8c168742f65e7397d41527c037b587fad8c", - "result_blake3": "b1f074e39a8bbafd9011fc375e78896bbcd53d9d4fa96921b3ff9ff1801aca8c", - "receipt_digest": "eba67384458c440da32547c18efa375459be28929066ed424fabda108163722e", - "job_blake3": "9d8cd6babbce3e4e5ec5e20e12ee073dc00619a764159f7beb92eb5e912415da" + "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/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" } } ], diff --git a/wai/wai-quantum-cli/Cargo.lock b/wai/wai-quantum-cli/Cargo.lock index a9d551434..ab24ab25c 100644 --- a/wai/wai-quantum-cli/Cargo.lock +++ b/wai/wai-quantum-cli/Cargo.lock @@ -1723,6 +1723,7 @@ name = "wai-wasmq" version = "0.1.0" dependencies = [ "blake3", + "serde", "serde_json", "wai-quantum", "wasmparser", diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 372847159..1028f1d92 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -1107,6 +1107,7 @@ Each construct below is supported (read as the language defines it), lowered (re | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | #### Timing @@ -1235,7 +1236,7 @@ Every code: | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000). | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -1262,6 +1263,7 @@ 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). | | `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. | @@ -1943,9 +1945,10 @@ Options: [default: 10000000] --fuel-per-job - The most fuel the whole run may use + 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: 1000000000000] + [default: 17179869184] --fuel-per-instance The most fuel an instance's seed, priors and init may use diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index c2fa94a3e..e9dd266e3 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21108,11 +21108,53 @@ "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", + "description": "A wasmQ job, as `wai-quantum verify` judges it: valid when its seal holds and it is a job the profile writes.", + "required": [ + "valid", + "signature", + "key", + "outcome", + "problems" + ], + "additionalProperties": false, + "properties": { + "valid": { + "type": "boolean" + }, + "signature": { + "type": "boolean", + "description": "Whether the receipt, of the form the engine seals, seals the request and the result under the key it names." + }, + "key": { + "type": [ + "string", + "null" + ], + "description": "The signing key, 32 bytes in hex." + }, + "outcome": { + "type": [ + "string", + "null" + ], + "description": "How the run ended, from the signed result: ok or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "What makes the job other than one the profile writes; empty when it is one." + } + } + }, { "type": "object", "description": "Whether a job's receipt seals its request and its result, and the key that signed it.", diff --git a/wai/wai-quantum-cli/schemas/local-tools.v1.json b/wai/wai-quantum-cli/schemas/local-tools.v1.json index 63cb70dcc..ffae06c45 100644 --- a/wai/wai-quantum-cli/schemas/local-tools.v1.json +++ b/wai/wai-quantum-cli/schemas/local-tools.v1.json @@ -5,8 +5,50 @@ "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", + "description": "A wasmQ job, as `wai-quantum verify` judges it: valid when its seal holds and it is a job the profile writes.", + "required": [ + "valid", + "signature", + "key", + "outcome", + "problems" + ], + "additionalProperties": false, + "properties": { + "valid": { + "type": "boolean" + }, + "signature": { + "type": "boolean", + "description": "Whether the receipt, of the form the engine seals, seals the request and the result under the key it names." + }, + "key": { + "type": [ + "string", + "null" + ], + "description": "The signing key, 32 bytes in hex." + }, + "outcome": { + "type": [ + "string", + "null" + ], + "description": "How the run ended, from the signed result: ok or fault." + }, + "problems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "What makes the job other than one the profile writes; empty when it is one." + } + } + }, { "type": "object", "description": "Whether a job's receipt seals its request and its result, and the key that signed it.", diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 5e99e2520..6a0f01f2d 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -575,6 +575,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,…")?; @@ -1201,7 +1227,10 @@ fn main() { #[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, seed and method: give them to `pack-wasmq`"); + 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) @@ -1210,6 +1239,8 @@ fn main() { 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, .. } => { @@ -1427,7 +1458,10 @@ fn main() { let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}"))); #[cfg(feature = "wasmq")] if v["result"]["format"] == wai_wasmq::run::FORMAT { - std::process::exit(if wasmq::verify(&v, bundle.as_deref()) { 0 } else { 1 }); + if hist.is_some() { + die("--history checks a program's run against its history; a wasmQ job carries none, so nothing could be checked"); + } + std::process::exit(if wasmq::verify(&src, bundle.as_deref()) { 0 } else { 1 }); } if bundle.is_some() { die("--bundle is for a wasmQ job (`run ENVELOPE.wai --json`)"); diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index d685c6dbb..7d3b04ea7 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -628,6 +628,12 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli if given["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { return Ok(wai_quantum::engine::variational::verify(given)); } + // A wasmQ job as `wai-quantum verify` judges it: its body, its form and its seal. + #[cfg(feature = "wasmq")] + if given["result"]["format"] == wai_wasmq::run::FORMAT { + let v = wai_wasmq::verify(given, None, wai_wasmq::run::EngineKind::default_here())?; + return Ok(json!({ "valid": v.ok(), "signature": v.signature, "key": v.signer, "outcome": v.outcome, "problems": v.well_formed })); + } let (valid, key) = job::verify(given)?; Ok(json!({ "valid": valid, "key": key })) } @@ -1179,6 +1185,39 @@ 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"); + // A run that faulted, its body edited to say it was done: the seal holds, the job does not. + let mut edited = job.clone(); + edited["status"] = json!("done"); + let v = tool("verify_quantum_result", json!({ "job": edited })); + assert_eq!(v["structuredContent"]["valid"], false, "{v}"); + assert_eq!(v["structuredContent"]["signature"], true, "{v}"); + } + #[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(); diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index f18ec9fc4..3fe653e5f 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -73,8 +73,8 @@ pub struct PackArgs { /// 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 - #[arg(long, default_value_t = 1_000_000_000_000)] + /// 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)] @@ -97,6 +97,13 @@ pub struct RunArgs { 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() } @@ -104,7 +111,7 @@ fn module_name(p: &Path) -> 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(&a.program)).unwrap_or_else(|_| die("the program is not UTF-8")); + 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 { @@ -177,34 +184,75 @@ pub fn run(file: &Path, json_out: bool, top: usize, key: &wai_quantum::ed25519_d None => EngineKind::default_here(), }; if let Some(path) = &a.replay { - let text = String::from_utf8(read(path)).unwrap_or_else(|_| die("the transcript is not UTF-8")); - let calls: Vec = text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty()).map(|(k, l)| record_of(k + 1, l)).collect(); - let problems = wai_wasmq::run::replay(&o, &calls, kind).unwrap_or_else(|e| die(&e.to_string())); - // 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. - let digest = wai_wasmq::run::transcript_blake3(&o, &calls); + // 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": calls.len(), "problems": problems, "transcript_blake3": digest })); + 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", calls.len()); - if problems.is_empty() { + println!("replayed {} calls through the envelope's modules", r.calls); + if ok { println!("result each call's output and fuel as the transcript records"); } - if let Some(d) = &digest { + // 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 &problems { + 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 problems.is_empty() { 0 } else { 1 }) - } - let (body, log) = wai_wasmq::run(&o, kind, key, a.transcript.is_some()).unwrap_or_else(|e| die(&e.to_string())); - if let (Some(path), Some(log)) = (&a.transcript, log) { - let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); - let text: String = log.iter().map(|c| json!({ "shot": c.shot, "callout": c.callout, "input": hex(&c.input), "output": hex(&c.output), "fuel": c.fuel }).to_string() + "\n").collect(); - std::fs::write(path, text).unwrap_or_else(|e| die(&format!("{}: {e}", path.display()))); + 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 { @@ -240,8 +288,18 @@ fn print_job(body: &Value, top: usize, kind: EngineKind) { println!("receipt digest {}, signed by {}", r["receipt"]["digest"].as_str().unwrap_or(""), r["receipt"]["key"].as_str().unwrap_or("")); } -/// `verify` for a wasmQ job: exit status 0 when it holds. -pub fn verify(v: &Value, bundle: Option<&Path>) -> bool { +/// `verify` for a wasmQ job, from its text: exit status 0 when it holds. +pub fn verify(text: &str, bundle: Option<&Path>) -> bool { + // Read strictly first: a member twice, or a number readers take differently, makes the + // text mean different jobs to different readers (§6.5). + let v = match wai_wasmq::run::read_job(text) { + Ok(v) => v, + Err(e) => { + println!("text NOT a job the profile writes: {e}"); + return false; + } + }; + let v = &v; let opened = bundle.map(|p| wai_wasmq::open(&read_envelope(p)).unwrap_or_else(|r| die(&r.to_string()))); let verdict = wai_wasmq::verify(v, opened.as_ref(), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); println!("signature {}", if verdict.signature { "VALID" } else { "INVALID" }); diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index a9bb1bc1d..d8fe530ee 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -242,6 +242,10 @@ fn a_transcript_line_that_is_no_record_is_refused_and_a_replay_states_the_digest 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); @@ -270,3 +274,94 @@ fn a_file_past_the_profile_s_limits_is_refused_before_it_is_read() { 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_strictly_before_it_is_checked() { + 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)); + // 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", j]); + assert!(!o.status.success(), "a whole number past 2^64: {}", text(&o)); + // A number with a fraction, where the profile writes whole numbers. + std::fs::write(&jf, job.replacen("\"shots\":4", "\"shots\":4.0", 1)).unwrap(); + let o = run(&d, &["verify", j]); + assert!(!o.status.success(), "a number written with a fraction: {}", text(&o)); +} diff --git a/wai/wai-quantum/docs/qasm-diagnostics.md b/wai/wai-quantum/docs/qasm-diagnostics.md index 06322fd70..4684ac762 100644 --- a/wai/wai-quantum/docs/qasm-diagnostics.md +++ b/wai/wai-quantum/docs/qasm-diagnostics.md @@ -88,7 +88,7 @@ engine. A test holds this page to the code's table. | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000). | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -115,6 +115,7 @@ 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). | | `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 6213d823b..bce9a0412 100644 --- a/wai/wai-quantum/docs/qasm-feature-matrix.md +++ b/wai/wai-quantum/docs/qasm-feature-matrix.md @@ -149,6 +149,7 @@ and in the browser build. | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | ## Timing diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index 352a8aa12..8e6ff25b2 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -61,6 +61,10 @@ use crate::quantum_tableau::Clifford; /// The most qubits a program declares. pub const MAX_QUBITS: u32 = 65_536; +/// The deepest the reader nests statements, operators and brackets: a program nested +/// deeper is refused ([`diag::TOO_DEEP`]) before it could use up a reader's stack, so that +/// every host, a small one among them, reads it the same way. +pub const MAX_NESTING: u32 = 128; /// The most gates a program expands to. pub const MAX_OPS: usize = 2_000_000; @@ -441,9 +445,40 @@ pub fn read_with(src: &str, inputs: &Inputs) -> Result { /// (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) = parse::parse(src, &toks)?; - let (program, lines, diagnostics, externs, calls) = elab::elaborate_callouts(src, &toks, &stmts, version, inputs)?; + let (program, lines, diagnostics, externs, calls) = elab::elaborate_callouts(src, &toks, &stmts, 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 }; diff --git a/wai/wai-quantum/src/qasm/diag.rs b/wai/wai-quantum/src/qasm/diag.rs index 60d9a1589..ca66bcf53 100644 --- a/wai/wai-quantum/src/qasm/diag.rs +++ b/wai/wai-quantum/src/qasm/diag.rs @@ -212,7 +212,7 @@ table! { GLOBAL_PHASE = "Q206", Lowered, "`gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls."; NO_CONTROLS = "Q207", Refused, "A control modifier that adds no control: `ctrl(0) @`."; TOO_MANY_CONTROLS = "Q208", Refused, "Modifiers that add more controls than the reader lowers (16)."; - TOO_DEEP = "Q209", Refused, "Gate definitions or subroutine calls nested deeper than 32."; + TOO_DEEP = "Q209", Refused, "Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128."; TOO_MANY_GATES = "Q210", Refused, "A program that expands to more gates than the reader holds (2 000 000)."; MODIFIER_PLACE = "Q211", Refused, "A modifier on something that is not a gate."; GATE_BODY = "Q212", Refused, "A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading."; @@ -241,6 +241,7 @@ 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)."; // 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 b8086544e..9fedf39aa 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -233,6 +233,12 @@ pub(crate) struct Elab<'a> { 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, @@ -272,19 +278,35 @@ pub(crate) fn elaborate_with(src: &str, toks: &[Token], stmts: &[Stmt], version: } /// [`elaborate`] with each call of an `extern` read as a step (`qasm::read_callouts`). -pub(crate) fn elaborate_callouts(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs) -> R<(Program, Vec, Vec, Vec, Vec)> { +/// 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], version: Version, inputs: &Inputs, bounds: super::Bounds) -> R { let mut e = elab(src, toks, version, inputs, None, true); + 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); -fn elaborate_full(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs, tan: Option, callouts: bool) -> R<(Program, Vec, 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); + +fn elaborate_full(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs, tan: Option, callouts: bool) -> R { let mut e = elab(src, toks, version, inputs, tan, callouts); e.prescan(toks, stmts)?; e.block(stmts)?; @@ -321,6 +343,9 @@ fn elab<'a>(src: &'a str, toks: &'a [Token], version: Version, inputs: &'a Input 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(), @@ -369,9 +394,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 // ----------------------------------------------------------------------- @@ -382,9 +434,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); @@ -482,6 +535,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")); } @@ -761,6 +815,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( @@ -877,6 +932,11 @@ impl<'a> Elab<'a> { 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(); @@ -1132,6 +1192,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()))); @@ -1210,6 +1273,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) => { @@ -1358,8 +1422,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) } @@ -1525,12 +1603,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 }); @@ -2396,6 +2476,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 @@ -2893,7 +2974,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) => { @@ -3765,7 +3865,7 @@ impl<'a> Elab<'a> { self.finish_with_calls().map(|(p, l, d, _)| (p, l, d)) } - fn finish_with_calls(mut self) -> R<(Program, Vec, Vec, Vec)> { + 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")); } @@ -3784,7 +3884,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 })); diff --git a/wai/wai-quantum/src/qasm/parse.rs b/wai/wai-quantum/src/qasm/parse.rs index 37b916df9..a09834676 100644 --- a/wai/wai-quantum/src/qasm/parse.rs +++ b/wai/wai-quantum/src/qasm/parse.rs @@ -21,6 +21,8 @@ pub struct Parser<'a> { toks: &'a [Token], pos: usize, pub version: Version, + /// How deep the statements and expressions being read are nested now. + depth: u32, } type R = Result; @@ -30,7 +32,7 @@ const TYPES: [&str; 10] = ["bit", "int", "uint", "float", "angle", "bool", "comp impl<'a> Parser<'a> { pub fn new(src: &'a str, toks: &'a [Token]) -> Self { - Parser { src, toks, pos: 0, version: Version::Unstated } + Parser { src, toks, pos: 0, version: Version::Unstated, depth: 0 } } /// How many of its tokens have been read: a reader of another grammar @@ -180,7 +182,25 @@ impl<'a> Parser<'a> { self.expect(";").map(|_| ()) } + /// One level deeper: a statement in a body or block, or an operand of an operator or + /// a bracket. Past [`super::MAX_NESTING`] the program is refused, on every host alike, + /// before a reader's own stack could run out on it. + fn deeper(&mut self) -> R<()> { + self.depth += 1; + if self.depth > super::MAX_NESTING { + return self.err(diag::TOO_DEEP, format!("statements and expressions nest deeper than {}", super::MAX_NESTING)); + } + Ok(()) + } + pub fn statement(&mut self) -> R { + self.deeper()?; + let s = self.statement_nested(); + self.depth -= 1; + s + } + + fn statement_nested(&mut self) -> R { let mut anns = Vec::new(); while let Some(Tok::Annotation(k, r)) = self.peek() { anns.push((k.clone(), r.clone(), self.here())); @@ -766,6 +786,13 @@ impl<'a> Parser<'a> { } fn unary(&mut self) -> R { + self.deeper()?; + let e = self.unary_nested(); + self.depth -= 1; + e + } + + fn unary_nested(&mut self) -> R { let at = self.here(); for op in ["-", "!", "~", "+"] { if self.eat(op) { 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-wasmq/Cargo.lock b/wai/wai-wasmq/Cargo.lock index 1bbed7fcf..f5a4162eb 100644 --- a/wai/wai-wasmq/Cargo.lock +++ b/wai/wai-wasmq/Cargo.lock @@ -1011,6 +1011,7 @@ name = "wai-wasmq" version = "0.1.0" dependencies = [ "blake3", + "serde", "serde_json", "wai-quantum", "wasmparser 0.261.0", diff --git a/wai/wai-wasmq/Cargo.toml b/wai/wai-wasmq/Cargo.toml index e7185b7d1..1266cf48d 100644 --- a/wai/wai-wasmq/Cargo.toml +++ b/wai/wai-wasmq/Cargo.toml @@ -27,6 +27,8 @@ wasmtime = ["dep:wasmtime"] [dependencies] blake3 = "1" +# A job is read strictly (`run::read_job`): a visitor of serde's own refuses a member twice. +serde = "1" serde_json = { version = "1", features = ["float_roundtrip"] } wai-quantum = { version = "0.8.1", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt"] } wasmparser = { version = "0.261", default-features = false, features = ["std", "validate", "features", "simd"] } diff --git a/wai/wai-wasmq/README.md b/wai/wai-wasmq/README.md index cbcccda18..2c76a53c2 100644 --- a/wai/wai-wasmq/README.md +++ b/wai/wai-wasmq/README.md @@ -8,7 +8,8 @@ An OpenQASM 3 program declares the classical functions it calls between gates wi 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; + 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 @@ -33,3 +34,7 @@ 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/mutations.py b/wai/wai-wasmq/mutations.py new file mode 100644 index 000000000..5b6ce3806 --- /dev/null +++ b/wai/wai-wasmq/mutations.py @@ -0,0 +1,164 @@ +#!/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). The tests are wasmQ's, but for a file of wai-quantum tested by +# that crate's own `--lib` tests, 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 body's status", WASMQ, "src/run.rs", ' "status" => out.push(format!("the body\'s status is {value}, where its signed result says {}", body_status(r))),', ' "status" => {}', ["--test", "review"], ["a_job_whose_status_or_method"]), + ("the signer is the receipt's key", WASMQ, "src/run.rs", "signer: key.as_str().map(str::to_string)", "signer: None", ["--test", "review"], ["a_job_names_its_signer"]), + ("the seal is checked only in its form", WASMQ, "src/run.rs", " let signature = seal_form.is_empty() && {", " let signature = {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the receipt's algorithm", WASMQ, "src/run.rs", ' if seal["alg"] != "ed25519" || seal["canon"] != "rfc8785" {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the result's members", WASMQ, "src/run.rs", ' members(r, "the result", &RESULT, &["fault"], &mut out);', "", ["--test", "sweep"], ["a_job_whose_result"]), + ("the contract's form", WASMQ, "src/run.rs", " contract_form(c, &mut out);", "", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the contract's method", WASMQ, "src/run.rs", ' if run["method"] != METHOD {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the contract's shots", WASMQ, "src/run.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", WASMQ, "src/run.rs", " if r[name] != want {", " if r[name] != want && false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("fuel used within the run's budget", WASMQ, "src/run.rs", ' if fuel["used"].as_u64().is_none_or(|u| u > per_job) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a call within the call's budget", WASMQ, "src/run.rs", ' if f["max"].as_u64().is_none_or(|m| m > per_call) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a callout's fuel within its calls", WASMQ, "src/run.rs", ' if f["fuel"].as_u64().is_none_or(|all| f["calls"]', ' if false && f["fuel"].as_u64().is_none_or(|all| f["calls"]', ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the instances' fuel", WASMQ, "src/run.rs", ' if fuel["instances"].as_u64().is_none_or(|n| u128::from(n) > made * stretches * u128::from(per_instance)) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("a fault's fuel within its phase's budget", WASMQ, "src/run.rs", ' if f["fuel"].as_u64().is_none_or(|n| n > budget) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("the fuel adds up", WASMQ, "src/run.rs", ' if spent != fuel["used"].as_u64() {', ' if spent != fuel["used"].as_u64() && false {', ["--test", "sweep"], ["a_job_whose_result"]), + ("a fault after every shot", WASMQ, "src/run.rs", " if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) {", " if false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("the counts' sum", WASMQ, "src/run.rs", " if total != shots_run {", " if 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 strictly", CLI, "src/wasmq.rs", " let v = match wai_wasmq::run::read_job(text) {", " let v = match serde_json::from_str::(text).map_err(|e| e.to_string()) {", ["--test", "wasmq"], ["a_job_is_read_from_its_text_strictly"]), + ("no history for a wasmQ job", CLI, "src/main.rs", " if hist.is_some() {\n die(", " if false {\n die(", ["--test", "wasmq"], ["a_flag_that_does_not_apply"]), +] + + +def run(name, crate, path, old, new, args, tests): + 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. + tested = crate if "--lib" 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 index a13dee4e0..0ea65e28a 100644 --- a/wai/wai-wasmq/src/abi.rs +++ b/wai/wai-wasmq/src/abi.rs @@ -32,6 +32,12 @@ 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) { diff --git a/wai/wai-wasmq/src/engine.rs b/wai/wai-wasmq/src/engine.rs index 83242ebb8..ed459c2b7 100644 --- a/wai/wai-wasmq/src/engine.rs +++ b/wai/wai-wasmq/src/engine.rs @@ -151,7 +151,7 @@ impl Interpreter<'_> { // --------------------------------------------------------------------------- #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] -pub use native::{Native, native_fuel}; +pub use native::{Native, native_fuel, on_its_stack}; #[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))] mod native { @@ -168,6 +168,27 @@ mod native { /// 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; @@ -340,6 +361,9 @@ mod native { /// 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}"))), diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index ab75b73ab..9ddf64508 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -31,15 +31,42 @@ pub const METHOD: &str = "stabilizer"; /// The most bytes an envelope holds (§2.3): a reader holds them and its frames at once, /// and what one host opens every host must be able to. pub const MAX_ENVELOPE_BYTES: usize = 1 << 27; +/// The most bytes a program's text may have (§4.8): a reader holds its tokens, some eighty +/// bytes for each byte of text, before it reads a statement. +pub const MAX_PROGRAM_BYTES: usize = 1 << 20; /// 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 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 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 most bytes of module the modules of a bundle hold among them (§3.2, §4.8). +pub const MAX_CODE_BYTES: usize = 1 << 19; /// The most bits a run's results may hold: its shots times the bits of a shot (§4.8). pub const MAX_RESULT_BITS: u128 = 1 << 28; -/// The most work a run may ask of its host outside its modules (§4.8). +/// The most work a run may ask of its host (§4.8): the host's own part, and the most fuel +/// it may be given. pub const MAX_HOST_WORK: u128 = 1 << 42; /// The host's part of a call into a module, or of making an instance, as [`host_work`] -/// counts it: on the native engine, about 50 microseconds, for a thread of its own. -const CALL_WORK: u128 = 1 << 16; +/// counts it. +pub const CALL_WORK: u128 = 1 << 12; +/// 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; +/// A unit of fuel, in the host's work: what the slowest engine, the interpreter under +/// wasm32, takes to run it (§4.2). +pub const FUEL_WORK: u128 = 8; +/// 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). +pub const CODE_WORK: u128 = 1 << 17; +/// 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() @@ -82,7 +109,7 @@ pub struct Run { 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_000_000_000_000, per_init: 100_000_000 } + Run { shots, seed, method: String::new(), state: "shot".into(), per_call: 10_000_000, per_job: 1 << 34, per_init: 100_000_000 } } } @@ -113,7 +140,10 @@ fn refuse(code: &'static str, msg: impl Into) -> Refusal { /// 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::*; - let h = wai_quantum::qasm::read_callouts(program, inputs).map_err(|d| refuse("W016", format!("{}: {}", d.code, d.message)))?; + 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")); @@ -121,18 +151,13 @@ pub fn read(program: &str, inputs: &wai_quantum::qasm::Inputs) -> Result<(wai_qu 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))); } - if p.n_qubits > MAX_QUBITS { - return Err(refuse("W016", format!("{} qubits; the method holds at most {MAX_QUBITS}", p.n_qubits))); - } - if h.steps.len() > MAX_STEPS { - return Err(refuse("W016", format!("{} steps, its loops unrolled; a program takes at most {MAX_STEPS}", h.steps.len()))); - } Ok((h, METHOD.to_string())) } -/// The work a run asks of its host that no fuel counts (§4.8), in operations on a word: -/// for each shot a fresh tableau and the program's steps, and for each instance the calls -/// that make it and its memory. (Fuel counts a prior's bytes, as it does a module's work.) +/// 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); @@ -141,23 +166,35 @@ pub fn host_work(h: &wai_quantum::qasm::Hybrid, facts: &[profile::Facts], b: &Bu for s in &h.steps { shot += match s { Step::Instr(Instr::Gate(_)) => r, - Step::Instr(Instr::Measure { .. } | Instr::Reset(_)) => n * r, - Step::Callout { input, output, .. } => CALL_WORK + ((input.len() + output.len()) as u128).div_ceil(64), + 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 + m.priors.len() + usize::from(f.functions.contains_key("wasmq-seed")) + usize::from(f.functions.contains_key("init")); - instance += CALL_WORK * calls as u128 + f.memory.map_or(0, |(initial, _)| u128::from(initial)) * 8192; + 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 + 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}"))); @@ -200,7 +237,7 @@ pub fn check(b: &Bundle) -> Result { } let work = host_work(&p, &facts, b); if work > MAX_HOST_WORK { - return Err(refuse("W017", format!("{work} operations on the host's part, which no fuel counts; a run asks at most {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) { diff --git a/wai/wai-wasmq/src/interp.rs b/wai/wai-wasmq/src/interp.rs index dc6cee5f9..fb9636ac6 100644 --- a/wai/wai-wasmq/src/interp.rs +++ b/wai/wai-wasmq/src/interp.rs @@ -215,6 +215,7 @@ impl<'a> Module<'a> { // 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)?; @@ -232,6 +233,7 @@ impl<'a> Module<'a> { } } 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 { @@ -323,7 +325,7 @@ impl<'a> Module<'a> { } ops.finish().map_err(err)?; allocs = fv.into_allocations(); - let weight = profile::frame_weight(params, results, locals, body.range().end - body.range().start); + 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 }); } } @@ -836,7 +838,7 @@ impl<'a> Instance<'a> { MemorySize { .. } => self.stack.push(self.pages), MemoryGrow { .. } => { let delta = self.pop() as u32 as u64; - self.charge(delta)?; + 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); diff --git a/wai/wai-wasmq/src/meter.rs b/wai/wai-wasmq/src/meter.rs index 5b33842d8..79e873fe3 100644 --- a/wai/wai-wasmq/src/meter.rs +++ b/wai/wai-wasmq/src/meter.rs @@ -8,7 +8,7 @@ //! - 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 the page count; +//! `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`]; @@ -51,8 +51,12 @@ pub fn ends_run(op: &Operator<'_>) -> bool { 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` one for each page. +/// 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 { .. }) @@ -160,15 +164,21 @@ fn fuel_check(out: &mut Vec, g: &Globals) { out.extend_from_slice(&[0x42, 0x00, 0x53, 0x04, 0x40, 0x00, 0x0b]); } -/// The charge of the i32 on top of the stack, kept there (in local `t`). -fn charge_top(out: &mut Vec, g: &Globals, t: u32) { +/// 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.extend_from_slice(&[0xad, 0x7d]); + 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); } @@ -302,7 +312,7 @@ pub fn prepare_with(bytes: &[u8], parts: Parts) -> Result, Refusal> { } for (op, a, b) in &list[i..=j] { if parts.fuel && variable(op) { - charge_top(&mut f, &g, t); + 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 { @@ -388,11 +398,12 @@ mod tests { } /// The largest growth preparing can cause: a body at the profile's limit that is all - /// calls, with as many globals, functions and types as the profile takes, so that - /// every added index is as long as it gets, and a float so the two functions are added. + /// 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, MAX_ITEMS}; + use crate::profile::MAX_BODY_BYTES; let leb = |v: u64| { let mut b = Vec::new(); leb_u(&mut b, v); @@ -404,17 +415,16 @@ mod tests { s.extend(body); s }; - let n = MAX_ITEMS as u64; - // MAX_ITEMS - 2 types `() -> ()` (preparing adds two), and as many functions. - let mut types = leb(n - 2); - for _ in 0..n - 2 { + // 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 - 2); - funcs.extend(std::iter::repeat_n(0u8, (n - 2) as usize)); - // MAX_ITEMS - 2 globals: the two added ones then have the longest indices. - let mut globals = leb(n - 2); - for _ in 0..n - 2 { + 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. @@ -423,11 +433,10 @@ mod tests { big.extend_from_slice(&[0x10, 0x01]); } big.push(0x0b); - assert!(big.len() as u64 >= MAX_BODY_BYTES - 1); - let mut code = leb(n - 2); + let mut code = leb(n); code.extend(leb(big.len() as u64)); code.extend_from_slice(&big); - for _ in 0..n - 3 { + for _ in 0..n - 1 { code.extend_from_slice(&[0x02, 0x00, 0x0b]); } let mut m = b"\0asm\x01\0\0\0".to_vec(); @@ -436,10 +445,11 @@ mod tests { } profile::check(&m).unwrap(); let p = prepare(&m).unwrap(); - // The body grew by about thirty times, and is still under the 7,654,321 bytes an - // engine takes for one function. - let growth = p.len() as f64 / m.len() as f64; - assert!(p.len() > 3_000_000 && p.len() < 7_000_000, "{} bytes, {growth:.1} times", p.len()); + // 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] @@ -452,8 +462,28 @@ mod tests { .unwrap(); let a = prepare(&m).unwrap(); assert_eq!(a, prepare(&m).unwrap()); - // The original's code bytes all appear, in order, in the prepared code. - assert!(a.len() > m.len()); + // 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 index 2460a047d..beefd25ab 100644 --- a/wai/wai-wasmq/src/profile.rs +++ b/wai/wai-wasmq/src/profile.rs @@ -20,8 +20,8 @@ use wasmparser::{ pub const PROFILE: &str = "wai.quantum.wasmq/1"; /// 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 (4 MiB). -pub const MAX_MODULE_BYTES: usize = 4 << 20; +/// 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; @@ -31,8 +31,9 @@ pub const MAX_EXPORTS: u32 = 1 << 10; 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 (128 KiB). -pub const MAX_BODY_BYTES: u64 = 1 << 17; +/// 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). @@ -121,9 +122,10 @@ pub fn parser() -> Parser { } /// The weight of a function's frame (§4.3): a base, and one for each parameter, result -/// and local and each byte of its entry in the code section. -pub fn frame_weight(params: u32, results: u32, locals: u32, body_bytes: u64) -> u32 { - FRAME_BASE + params + results + locals + body_bytes as u32 +/// 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. @@ -373,10 +375,12 @@ pub fn check(bytes: &[u8]) -> Result { 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}"))); @@ -386,7 +390,7 @@ pub fn check(bytes: &[u8]) -> Result { 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, size)); + facts.weights.push(frame_weight(params as u32, results, locals as u32, operands, size)); } wasmparser::ValidPayload::End(_) => {} _ => {} @@ -609,8 +613,11 @@ mod tests { 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). + // 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 + 6]); + 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/run.rs b/wai/wai-wasmq/src/run.rs index d86b1f84c..dcca0431f 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -155,17 +155,38 @@ fn metered(inst: &mut dyn Instance, b: &mut Budget, per: u64, own: &'static s (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 (m, p) in b.modules.iter().zip(prepared) { + 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)?), + 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)?), + EngineKind::InterpreterPrepared => Box::new(engine::Interpreter::new(p()?, interp::Metering::Module).map_err(Error::Host)?), }); } Ok(out) @@ -204,10 +225,22 @@ fn instance<'e>(b: &Bundle, k: usize, facts: &profile::Facts, engine: &'e dyn en /// 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> { - let b = &opened.bundle; - let prepared: Vec> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; - let engines = engines(b, &prepared, kind)?; - execute(opened, &engines, key, log) + 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) + }) } fn transcript_start() -> blake3::Hasher { @@ -256,7 +289,7 @@ pub(crate) fn width(p: &Program, steps: &[Step]) -> usize { w } -fn execute<'e>(opened: &Opened, engines: &'e [Box], key: &SigningKey, log: bool) -> Result<(Value, Option>), Error> { +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); @@ -270,7 +303,6 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key let (mut used, mut init, mut calls) = (0u64, 0u64, 0u64); let mut by_callout: BTreeMap = BTreeMap::new(); let mut transcript = transcript_start(); - let mut record = log.then(Vec::new); let mut counts: BTreeMap = BTreeMap::new(); let mut shots_run = 0u64; // The fault that stopped the run: where, and what. @@ -352,8 +384,8 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key bits[bit as usize] = outs[k]; } hash_call(&mut transcript, shot, *func, &in_bytes, &out_bytes, n); - if let Some(l) = record.as_mut() { - l.push(Call { shot, callout: decl.name.clone(), input: in_bytes, output: out_bytes, fuel: 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)), @@ -411,7 +443,7 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key } result["fault"] = f; } - Ok((seal(opened, result, key), record)) + Ok(seal(opened, result, key)) } /// Replays recorded calls against a bundle's modules (extensions/quantum-wasmq.md §7): each @@ -420,28 +452,69 @@ fn execute<'e>(opened: &Opened, engines: &'e [Box], key /// 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: Vec> = b.modules.iter().map(|m| meter::prepare(&m.bytes)).collect::>().map_err(Error::Refused)?; + 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 problems = Vec::new(); + 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.iter().enumerate() { + 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 { - problems.push(format!("call {k}: shot {}, where the run has {} shots", 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 { - problems.push(format!("call {k}: shot {} after shot {last}: a run's calls come in the order of its shots", call.shot)); + 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 { - problems.push(format!("call {k}: the program declares no `extern {}`", call.callout)); + problem(&mut out, format!("call {k}: the program declares no `extern {}`", call.callout)); continue; }; let decl = &externs[func]; @@ -455,14 +528,14 @@ pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result live[c.module] = Some((call.shot, i)), Err(s) => { - problems.push(format!("call {k} ({} in shot {}): the module stops with {} in its {}", call.callout, call.shot, s.fault.reason, s.phase)); + 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) { - problems.push(format!("call {k} ({}): {} input bytes, where its arguments take {}", call.callout, 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(); @@ -472,19 +545,20 @@ pub fn replay(opened: &Opened, calls: &[Call], kind: EngineKind) -> Result { - let out = abi::pack(&outs); - if out != call.output { - problems.push(format!("call {k} ({} in shot {}): the module returns {}, the record says {}", call.callout, call.shot, hex(&out), hex(&call.output))); + 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 { - problems.push(format!("call {k} ({} in shot {}): the module uses {used} fuel, the record says {}", call.callout, call.shot, 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)) => problems.push(format!("call {k} ({} in shot {}): the module stops with {}", call.callout, call.shot, f.reason)), + 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)), } } - Ok(problems) + 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. @@ -531,6 +605,187 @@ 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'))) } +/// Whether `v` is a whole number from `lo` to `hi`. +fn whole(v: &Value, lo: u64, hi: u64) -> bool { + v.as_u64().is_some_and(|n| (lo..=hi).contains(&n)) +} + +/// 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) { + use crate::envelope::{MAX_CODE_BYTES, MAX_ENVELOPE_BYTES, MAX_EXACT, MAX_SHOTS, METHOD}; + let mut problem = |s: String| out.push(format!("the contract: {s}")); + let mut sub = Vec::new(); + members(c, "it", &["profile", "program", "modules", "callouts", "run"], &[], &mut sub); + members(&c["program"], "its program", &["blake3", "bytes", "inputs"], &[], &mut sub); + members(&c["run"], "its run", &["shots", "seed", "method", "state", "fuel"], &[], &mut sub); + members(&c["run"]["fuel"], "its budgets", &["per_call", "per_job", "per_instance"], &[], &mut sub); + for s in sub { + problem(s); + } + if c["profile"] != profile::PROFILE { + problem(format!("its profile is {}, not {}", c["profile"], profile::PROFILE)); + } + let p = &c["program"]; + if !is_digest(&p["blake3"]) || !whole(&p["bytes"], 1, crate::envelope::MAX_PROGRAM_BYTES as u64) { + problem(format!("its program is {}, not a digest and a length", p)); + } + if !p["inputs"].as_object().is_some_and(|m| m.values().all(|v| v.as_i64().is_some_and(|i| i.unsigned_abs() <= MAX_EXACT))) { + problem(format!("its program's inputs are {}, not whole numbers", p["inputs"])); + } + let modules = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if modules.is_empty() { + problem("it holds no module".into()); + } + let mut code = 0u64; + for m in modules { + let mut sub = Vec::new(); + members(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_CODE_BYTES as u64) { + problem(format!("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(m["bytes"].as_u64().unwrap_or(0)); + let priors = m["priors"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if !m["priors"].is_array() { + problem(format!("a module's priors are {}", m["priors"])); + } + for pr in priors { + let mut sub = Vec::new(); + members(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_ENVELOPE_BYTES as u64) { + problem(format!("a prior is {pr}, not a name, a digest and a length")); + } + } + } + if code > MAX_CODE_BYTES as u64 { + problem(format!("its modules hold {code} bytes, past {MAX_CODE_BYTES}")); + } + let callouts = c["callouts"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if callouts.is_empty() { + problem("it names no callout".into()); + } + let mut externs = std::collections::BTreeSet::new(); + for x in callouts { + let mut sub = Vec::new(); + members(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!("a callout is {x}, not an extern, a module it holds and an export")); + } + if !externs.insert(x["extern"].to_string()) { + problem(format!("two callouts for the extern {}", x["extern"])); + } + } + for k in 0..modules.len() { + if !callouts.iter().any(|x| x["module"].as_u64() == Some(k as u64)) { + problem(format!("module {k} is named by no callout")); + } + } + let run = &c["run"]; + if !whole(&run["shots"], 1, MAX_SHOTS) { + problem(format!("its shots are {}, where a run takes 1 to {MAX_SHOTS}", run["shots"])); + } + if !whole(&run["seed"], 0, MAX_EXACT) { + problem(format!("its seed is {}", run["seed"])); + } + if run["method"] != METHOD { + problem(format!("its method is {}, where this version of the profile has {METHOD}", run["method"])); + } + if run["state"] != "shot" && run["state"] != "job" { + problem(format!("its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); + } + let most = (crate::envelope::MAX_HOST_WORK / crate::envelope::FUEL_WORK) as u64; + for b in ["per_call", "per_job", "per_instance"] { + if !whole(&run["fuel"][b], 0, MAX_EXACT.min(most)) { + problem(format!("its fuel {b} is {}, past what a run may be given", run["fuel"][b])); + } + } +} + +/// Holds a receipt to the form the engine seals (§6.4): Ed25519 over the RFC 8785 form, a +/// key, a digest and a signature in lowercase hexadecimal. What else a receipt holds is +/// not the seal's, and a verdict takes nothing from it. +fn receipt_form(seal: &Value, out: &mut Vec) { + if !seal.is_object() { + out.push(format!("the receipt is {seal}, not an object")); + } + if seal["alg"] != "ed25519" || seal["canon"] != "rfc8785" { + out.push(format!("the receipt is of {} over {}, not ed25519 over rfc8785", seal["alg"], seal["canon"])); + } + for (name, len) in [("key", 64), ("digest", 64), ("signature", 128)] { + if !lower_hex(&seal[name], len) { + out.push(format!("the receipt's {name} is not {len} lowercase hexadecimal digits")); + } + } +} + +fn lower_hex(v: &Value, len: usize) -> bool { + v.as_str().is_some_and(|s| s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))) +} + +/// Reads a job from its text strictly, before anything is checked (§6.5): no member twice, +/// since readers differ on which they keep, and every number a whole number a double holds +/// exactly, written as one, since readers differ on any other. +pub fn read_job(text: &str) -> Result { + use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor}; + struct Strict(Value); + struct V; + impl<'de> Visitor<'de> for V { + type Value = Value; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a job of the profile") + } + fn visit_bool(self, v: bool) -> Result { + Ok(Value::Bool(v)) + } + fn visit_i64(self, v: i64) -> Result { + if v.unsigned_abs() > crate::envelope::MAX_EXACT { + return Err(E::custom(format!("{v} is a whole number past 2^53 - 1"))); + } + Ok(Value::from(v)) + } + fn visit_u64(self, v: u64) -> Result { + if v > crate::envelope::MAX_EXACT { + return Err(E::custom(format!("{v} is a whole number past 2^53 - 1"))); + } + Ok(Value::from(v)) + } + fn visit_f64(self, v: f64) -> Result { + Err(E::custom(format!("{v}: the profile writes only whole numbers, without a fraction or an exponent"))) + } + fn visit_str(self, v: &str) -> Result { + Ok(Value::String(v.to_string())) + } + fn visit_unit(self) -> Result { + Ok(Value::Null) + } + fn visit_seq>(self, mut seq: A) -> Result { + let mut v = Vec::new(); + while let Some(Strict(x)) = seq.next_element()? { + v.push(x); + } + Ok(Value::Array(v)) + } + fn visit_map>(self, mut map: A) -> Result { + let mut m = Map::new(); + while let Some(k) = map.next_key::()? { + let Strict(v) = map.next_value()?; + if m.insert(k.clone(), v).is_some() { + return Err(de::Error::custom(format!("the member {k:?} twice"))); + } + } + Ok(Value::Object(m)) + } + } + impl<'de> Deserialize<'de> for Strict { + fn deserialize>(d: D) -> Result { + d.deserialize_any(V).map(Strict) + } + } + serde_json::from_str::(text).map(|s| s.0).map_err(|e| e.to_string()) +} + /// What makes a job other than one this profile writes (§6.4, §6.5). A signature shows /// that a key sealed a job, and says nothing of what the job holds: so without the /// envelope to run again, this is what holds a result to its contract and to itself. @@ -566,6 +821,8 @@ fn form(job: &Value) -> Vec { members(r, "the result", &RESULT, &["fault"], &mut out); let c = &req["wasmq"]; let run = &c["run"]; + contract_form(c, &mut out); + receipt_form(&r["receipt"], &mut out); // What the result repeats from the contract, or fixes. let fixed = [("format", json!(FORMAT)), ("profile", json!(profile::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 { @@ -591,6 +848,9 @@ fn form(job: &Value) -> Vec { } let per_call = run["fuel"]["per_call"].as_u64().unwrap_or(0); let externs: Vec<&Value> = c["callouts"].as_array().into_iter().flatten().map(|x| &x["extern"]).collect(); + if !fuel["callouts"].is_object() { + out.push(format!("the fuel of the callouts is {}, not an object", fuel["callouts"])); + } let mut spent = fuel["instances"].as_u64(); let mut calls = Some(0u64); for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { @@ -607,6 +867,9 @@ fn form(job: &Value) -> Vec { if f["calls"].as_u64().is_none_or(|n| n == 0) { out.push(format!("callout {name} has fuel for {} calls", f["calls"])); } + if f["fuel"].as_u64().is_none_or(|all| f["calls"].as_u64().zip(f["max"].as_u64()).is_none_or(|(n, m)| u128::from(all) > u128::from(n) * u128::from(m))) { + out.push(format!("callout {name}'s fuel, {}, is past its {} calls of at most {} each", f["fuel"], f["calls"], f["max"])); + } spent = spent.zip(f["fuel"].as_u64()).and_then(|(a, b)| a.checked_add(b)); calls = calls.zip(f["calls"].as_u64()).and_then(|(a, b)| a.checked_add(b)); } @@ -624,6 +887,17 @@ fn form(job: &Value) -> Vec { } for m in &got { members(m, "a module of the result", &["name", "blake3"], &[], &mut out); + if !is_digest(&m["blake3"]) { + out.push(format!("a module of the result whose blake3 is {}, not a digest", m["blake3"])); + } + } + // 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 = run["fuel"]["per_instance"].as_u64().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(r["shots_run"].as_u64().unwrap_or(0)) + u128::from(r["status"] == "fault") }; + if fuel["instances"].as_u64().is_none_or(|n| u128::from(n) > made * stretches * u128::from(per_instance)) { + out.push(format!("the instances' fuel, {}, is past what {made} of them could be given", fuel["instances"])); } // How it ended. let (shots, shots_run) = (r["shots"].as_u64(), r["shots_run"].as_u64()); @@ -660,6 +934,10 @@ fn form(job: &Value) -> Vec { out.push(format!("a fault after {} fuel, of {} used", f["fuel"], fuel["used"])); } let is_call = phase == Some("call"); + let budget = if is_call { per_call } else { per_instance }; + if f["fuel"].as_u64().is_none_or(|n| n > budget) { + out.push(format!("a fault after {} fuel, past the {budget} its phase is given", f["fuel"])); + } match f.get("callout") { Some(x) if !is_call || !externs.contains(&x) => out.push(format!("a fault that names the callout {x} in phase {}", f["phase"])), None if is_call => out.push("a call's fault that names no callout".into()), @@ -730,8 +1008,22 @@ impl Verdict { /// profile writes (fuel within budgets, a fault reason it names); and, given the bundle, /// that it is the bundle the receipt binds and that running it again reproduces the result. pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result { - let (signature, key) = wai_quantum::engine::job::verify(job)?; - let r = &job["result"]; + let (Some(request), 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")?; + // The seal alone, as the engine checks a receipt: the key it names sealed this request + // and this result. Only a receipt of the form the engine seals is checked at all. + let mut seal_form = Vec::new(); + receipt_form(seal, &mut seal_form); + let signature = seal_form.is_empty() && { + let mut bare = r.clone(); + if let Some(o) = bare.as_object_mut() { + o.remove("receipt"); + } + wai_quantum::engine::receipt::verify(request, &bare, seal) + }; + let key = seal["key"].clone(); let c = &job["request"]["wasmq"]; let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), well_formed: form(job), ..Verdict::default() }; if let Some(o) = bundle { diff --git a/wai/wai-wasmq/src/vectors.rs b/wai/wai-wasmq/src/vectors.rs index e340a82b1..9a1823d4f 100644 --- a/wai/wai-wasmq/src/vectors.rs +++ b/wai/wai-wasmq/src/vectors.rs @@ -79,7 +79,8 @@ pub fn verify(dir: &std::path::Path, kinds: &[EngineKind]) -> Result<(Vec (Result, Trap> } #[test] -fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engine_s_own_count_where_no_growth_fails() { +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(); @@ -236,9 +236,9 @@ fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engine_s_own_count_whe 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 the pages a `memory.grow` asks for whether or not it - // gets them (§4.2); the engine's own meter charges them only when it does. - let unmet = if (*name, *arg) == ("mem", 4) { 4 } else { 0 }; + // 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"); } } @@ -292,63 +292,92 @@ fn the_stack_ends_at_the_same_call_on_every_engine() { } } +/// 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. -fn heavy_frames() -> Vec<(&'static str, Vec)> { +/// 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(); - // 4000 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. + // 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 = 4000; - 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)); + 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("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("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("local.get 0"); - out.push((if ty == "i64" { "4000 integers held across a call" } else { "4000 floats held across a call" }, wasm(format!("(module (memory 1 1) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", format!("{ty} ").repeat(n))))); - } - // The review's module, at the largest body the profile takes: forty parameters read - // at the end, and a new value held across two dozen calls at every step. - let steps = 8_000; - 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(); - out.push(( - "a value held across two dozen calls at each of 8000 steps", + 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 = 15_000; - 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"); - out.push(("the memory's size held across two dozen calls at each of 15000 steps", 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))))); + 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", + "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), @@ -357,7 +386,7 @@ fn heavy_frames() -> Vec<(&'static str, Vec)> { )), )); // The smallest frame there is. - out.push(("a function that only calls itself", wasm("(module (func $f (export \"f\") (param i64) (result i64) local.get 0 call $f))".to_string()))); + 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 } @@ -392,3 +421,109 @@ fn a_native_frame_takes_at_most_eight_bytes_for_each_unit_of_its_weight() { 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/reading.rs b/wai/wai-wasmq/tests/reading.rs new file mode 100644 index 000000000..fdf693f49 --- /dev/null +++ b/wai/wai-wasmq/tests/reading.rs @@ -0,0 +1,109 @@ +//! 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"); + let ifs = format!("{HEAD}{}x q[0];\n", "if (d == 1) ".repeat(depth - 2)); + assert!(read(&ifs).1.is_none(), "{depth} statements"); + 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/review.rs b/wai/wai-wasmq/tests/review.rs index cc9944a15..f608224a9 100644 --- a/wai/wai-wasmq/tests/review.rs +++ b/wai/wai-wasmq/tests/review.rs @@ -277,6 +277,17 @@ fn the_function_limit_is_one_a_module_can_reach() { // 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() @@ -380,15 +391,18 @@ fn rotating(steps: usize, depth: u64) -> Vec { #[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. - let m = rotating(40_000, 1023); - match profile::check(&m) { - // Refused on reading, by a limit the text states. - Err(e) => assert_eq!(e.code, "W010", "{e}"), - Ok(_) => { - let got = call_all(&m, "f", 0); - assert!(got.windows(2).all(|w| w[0] == w[1]), "the engines differ: {:?}", got.iter().map(|g| format!("{:?}", g.0).chars().take(90).collect::()).collect::>()); - } + // 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); } } @@ -482,26 +496,29 @@ fn a_measurement_or_reset_takes_a_value_only_when_its_outcome_is_random() { #[test] fn what_pack_accepts_every_engine_can_run() { - // `(func)` and an exported function of 183,000 calls of it: inside every rule the - // first text stated, and past an engine's limit once prepared. - let mut body = vec![0x00]; - for _ in 0..183_000 { - 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]); - let m = raw(&[types, section(3, &[0x02, 0x00, 0x01]), section(7, &[0x01, 0x01, b'f', 0x00, 0x01]), section(10, &code)]); - match pack(&bundle(SCALAR, m, vec![])) { - Err(e) => assert_eq!(e.code, "W010", "{e}"), - Ok(env) => { - let jobs = everywhere(&open(&env).unwrap()); - assert!(jobs.iter().all(|j| j.contains("\"status\"")), "packed, and then: {}", &jobs[0][..jobs[0].len().min(160)]); + // `(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] diff --git a/wai/wai-wasmq/tests/sweep.rs b/wai/wai-wasmq/tests/sweep.rs index 5d6fadbdb..250b887d7 100644 --- a/wai/wai-wasmq/tests/sweep.rs +++ b/wai/wai-wasmq/tests/sweep.rs @@ -58,6 +58,31 @@ fn modules_of(n: usize, pages: u64) -> Bundle { 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 @@ -67,8 +92,7 @@ fn the_modules_of_a_bundle_hold_no_more_memory_among_them_than_one_may() { assert_eq!(code(&modules_of(2, 2048)), None, "4096 pages among two"); } -/// One bit of the program's register measured: a register no step writes, in a program -/// that measures nothing, is not in its result (the reader measures every qubit instead). +/// One bit of the program's register measured. const ONE_MEASURED: &str = "c[0] = measure q[0];\n"; #[test] @@ -79,20 +103,20 @@ fn a_bundle_whose_result_a_host_could_not_hold_is_refused() { // 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 << 28, ONE_MEASURED), wasm(IDENTITY), 1)), None, "2^28 bits in one shot"); - assert_eq!(code(&one(program(1, (1 << 28) + 1, ONE_MEASURED), wasm(IDENTITY), 1)), Some("W017"), "one bit more"); + 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 random measurement at the most qubits takes about 4 ms and no fuel, so these would - // take a host far longer than anyone would wait, and it would seal nothing until then. + // 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 program that measures nothing is measured at its end, every qubit, and that counts. - assert_eq!(code(&one(program(32768, 2, ""), wasm(IDENTITY), 1 << 12)), Some("W017"), "the measurements the reader adds"); // 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"); @@ -101,50 +125,147 @@ fn a_run_no_host_could_finish_is_refused_though_it_spends_no_fuel() { b.run.state = "shot".into(); b.run.shots = 1 << 10; assert_eq!(code(&b), None, "the memory of a thousand instances"); - // And the same programs at a size a host finishes pack. - assert_eq!(code(&one(program(32768, 2, measures), wasm(IDENTITY), 1)), None, "these measurements, once"); - assert_eq!(code(&one(program(32768, 2, ONE_MEASURED), wasm(IDENTITY), 1 << 12)), None, "these tableaux, four thousand times"); - // A call costs the native engine about 50 microseconds, whatever fuel it uses. - let calls = "for uint i in [0:99] { d = f(d); }\nc[0] = measure q[0];\n"; - assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 20)), Some("W017"), "a hundred calls, a million times"); - assert_eq!(code(&one(program(1, 2, calls), wasm(IDENTITY), 1 << 10)), None, "a hundred calls, a thousand times"); + // 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() { - // §4.8, by hand: 64 qubits take r = 2 words a qubit. A tableau is 2 * 64 * 2 = 256; the - // call is 2^16 + 1; a gate is 2, twice; the measurement is 64 * 2 = 128; the jump is 1. - // 65926 for a shot. Three pages are 3 * 8192 = 24576, and making an instance 2^16. + 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); - assert_eq!(work(&b), 5 * 65926 + 5 * 90112, "an instance for each shot"); + 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 * 65926 + 90112, "one instance for the run"); - // A seed, an init and a prior are three more calls. (Fuel counts the prior's bytes.) - let mut b = one(src, wasm(r#"(module (memory (export "memory") 3 9) + 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))"#), 5); + (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 * 65926 + 5 * (90112 + 3 * 65536)); + 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() { - // 2^20 shots, so a shot and an instance may take 2^22 together. A tableau, the call and - // the measurement of one qubit are 2 + 65537 + 1, making the instance 65536, and 495 - // pages 4055040: which leaves 8188 for gates of one word. - let m = wasm(r#"(module (memory (export "memory") 495 495) (func (export "f") (param i64) (result i64) local.get 0))"#); + // 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 (h, _) = wai_wasmq::envelope::read(&with(8188).program, &Default::default()).unwrap(); - assert_eq!(wai_wasmq::envelope::host_work(&h, &[wai_wasmq::profile::check(&m).unwrap()], &with(8188)), wai_wasmq::envelope::MAX_HOST_WORK); - assert_eq!(code(&with(8188)), None, "exactly the limit"); - assert_eq!(code(&with(8189)), Some("W017"), "a gate more in each shot"); + 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] @@ -154,6 +275,14 @@ fn the_steps_of_a_program_are_bounded_at_their_limit() { 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. @@ -187,16 +316,40 @@ fn an_envelope_past_what_a_host_must_hold_is_refused() { assert!(open(&env).is_ok(), "an envelope within the limit"); } -/// A bundle at every limit a run holds in memory: the most memory, a prior of nearly all an -/// envelope holds, the most qubits and the most result, with two instances, and `calls` more. -fn the_largest(calls: u64) -> wai_wasmq::Opened { - let m = r#"(module (memory (export "memory") 4096 4096) +/// 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 body = format!("h q[0];\nc[0] = measure q[0];\nreset q[0];\nfor uint i in [0:{}] {{ d = f(d); }}\n", calls - 1); - let mut b = one(program(32768, 1 << 27, &body), wasm(m), 2); - b.modules[0].priors.push(("long".into(), vec![1; (1 << 27) - (1 << 16)])); + (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; @@ -214,14 +367,28 @@ fn the_largest_bundle_the_limits_admit_runs_on_every_engine() { #[test] fn the_largest_bundle_with_the_most_steps_fits_a_32_bit_host() { - // And with the most steps as well, on the interpreter, which is the engine wasm32 has: - // the native engine's thread for each of two million calls would only take minutes. - let o = the_largest((1 << 21) - 8); - assert_eq!(o.contract["run"]["shots"], 2); + // 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 @@ -243,7 +410,7 @@ fn every_jump_of_a_program_goes_forward_so_a_run_ends() { // --------------------------------------------------------------------------- fn a_job(module: &str, per_call: u64) -> (wai_wasmq::Opened, Value) { - let mut b = one(program(3, 1, "h q;\n"), wasm(module), 8); + 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(); @@ -373,6 +540,147 @@ const EDITS_OF_FAULT: &[Edit] = &[ }), ]; +/// 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); @@ -422,3 +730,39 @@ fn a_record_of_a_shot_the_run_does_not_have_or_out_of_order_does_not_replay() { 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 index bdf92d342..89ff02516 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -69,6 +69,100 @@ fn runs() -> Vec<(&'static str, Bundle)> { ("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)))); + // 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)); + out } fn leb(mut v: u64) -> Vec { @@ -185,11 +279,13 @@ fn bundle_refusals() -> Vec<(&'static str, &'static str, Bundle, Bundle)> { b }; add("memory-past-a-run-s-among-its-modules", "W003", of_memory(2049), of_memory(2048)); - let wide = |bits: u32| PROGRAM.replace("bit[8] r;", &format!("bit[8] r;\nbit[{bits}] pad;")); - add("results-past-what-a-host-holds", "W017", one(&wide(1 << 20), wasm(IDENTITY), Run::new(1 << 8, 1)), one(&wide((1 << 20) - 16), wasm(IDENTITY), Run::new(1 << 8, 1))); + // 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];"); - let calls = |n: u64| PROGRAM.replace("r = f(b);", &format!("for uint i in [0:{}] {{ r = f(b); }}", n - 1)); - add("program-past-its-steps", "W016", one(&calls(1 << 21), wasm(IDENTITY), Run::new(1, 1)), one(&calls((1 << 21) - 16), wasm(IDENTITY), Run::new(1, 1))); + // 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 } @@ -318,11 +414,12 @@ fn the_vectors_are_the_ones_made_here_and_hold_on_every_engine_of_this_build() { 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 fault a run vector can show is shown. - let faults: std::collections::BTreeSet<&str> = index["runs"].as_array().unwrap().iter().filter_map(|r| r["expect"]["fault"]["reason"].as_str()).collect(); - for reason in ["fuel_exhausted", "stack_exhausted", "integer_divide_by_zero", "bad_result"] { - assert!(faults.contains(reason), "no run vector ends in {reason}"); - } + // 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] -- 2.54.0 (Apple Git-157) From 3e61a8e66b52770602ad126b44a76ce826b7cbd8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 6 Nov 2026 08:45:37 -0500 Subject: [PATCH 07/17] wasmq: a verdict is the sealed content's, with the host's unsealed words noted beside it, and verify reads a job within a size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A body's status and method, and any member nothing seals, are the host's word: the verdict takes nothing from them, so that every surface that checks a job gives the same one. Where they differ from what is sealed, or are missing, verify and the MCP tool say so beside the verdict ("the host's unsealed status says "done"; the sealed result says "fault""), and the outcome reported is the sealed result's. A receipt's other members were already ignored. §6.4 and §6.5 say so. - verify reads at most 1 GiB, more than any job holds, and refuses a file past it by its length before reading it, whatever the job's kind; standard input is read no further. - mutations.py covers both: 82 checks. Co-Authored-By: Claude Opus 5.5 --- ...26-11-05-wasmq-hybrid-quantum-classical.md | 3 +- wai/extensions/quantum-wasmq.md | 17 ++++-- wai/wai-quantum-cli/docs/local-mcp.json | 12 +++- .../schemas/local-tools.v1.json | 12 +++- wai/wai-quantum-cli/src/main.rs | 33 +++++++++- wai/wai-quantum-cli/src/mcp.rs | 22 +++++-- wai/wai-quantum-cli/src/wasmq.rs | 6 +- wai/wai-quantum-cli/tests/wasmq.rs | 27 ++++++++- wai/wai-wasmq/mutations.py | 3 +- wai/wai-wasmq/src/run.rs | 60 ++++++++++++------- wai/wai-wasmq/tests/fuzz.rs | 10 +++- wai/wai-wasmq/tests/review.rs | 29 +++++++-- 12 files changed, 183 insertions(+), 51 deletions(-) diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index 5267f52a4..80f55bcc1 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -51,7 +51,8 @@ (`--engine`, `--transcript`, `--replay`, each record written and read a call at a time), and `verify JOB --bundle ENVELOPE.wai`, which reads the job's text strictly, names the signer and how the run ended, holds the contract, the result and the receipt to the forms the text gives - under whichever key, and runs the envelope again to compare byte for byte; the MCP server's + under whichever key, notes beside its verdict where the host's unsealed words differ from what + is sealed, and runs the envelope again to compare byte for byte; the MCP server's `verify_quantum_result` gives the same verdict. 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. diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 3f06bc88d..e5092662c 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -574,9 +574,9 @@ 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; `status` and `method` only repeat what is signed, and a body -where they differ from it, or that holds another member, is not a job of this profile -(§6.5). +request and the result; `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: @@ -612,8 +612,7 @@ since readers differ on such text. It then checks, and exits 0 only when all hol **signer**, the key the receipt carries, as a `did:key`. A receipt shows that this key sealed this request and this result; it does not say who holds the key, which is the reader's to compare with the signer it expects; -- that the body is one of this profile: its four members and no other, `status` and - `method` as the signed result gives them, and no whole number past 2^53 − 1; +- that the request and the result hold no whole number past 2^53 − 1; - that the contract is one §2.2 gives: its members and no other, each of its form and range, the method this version has, every module named by a callout; - that the request and the result are ones this profile writes, under whichever key: @@ -630,6 +629,14 @@ since readers differ on such text. It then checks, and exits 0 only when all hol Without the envelope this is all a check can hold a result to: a key can seal any result of this form. Only running the envelope again shows that a result is the one it gives. +The verdict is the sealed content's alone, so that every surface that checks a job gives +the same one. The body's unsealed `status` and `method`, and any other member nothing +seals, are the host's word: where they differ from what is sealed, or are missing, a check +says so beside the verdict ("the host's unsealed status says `done`; the sealed result says +`fault`"), and the verdict is unchanged. The outcome it reports is the sealed result's. +`verify` reads at most 1 GiB, more than any job holds, and refuses a file past it by its +size, before it reads it. + It prints how the run ended. With `--bundle ENVELOPE.wai` it also checks that the envelope is the one the receipt binds, and runs it again here: the result must be the same, byte for byte. A wasmQ job carries no history, so `--history` is refused for one. diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index e9dd266e3..5abb8bffe 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21121,7 +21121,8 @@ "signature", "key", "outcome", - "problems" + "problems", + "notes" ], "additionalProperties": false, "properties": { @@ -21151,7 +21152,14 @@ "items": { "type": "string" }, - "description": "What makes the job other than one the profile writes; empty when it is one." + "description": "What makes the job other than one the profile writes, from what is sealed; empty when it is one." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the host's unsealed words in the body (its status, its method, members nothing seals) differ from what is sealed: beside the verdict, and no part of it." } } }, diff --git a/wai/wai-quantum-cli/schemas/local-tools.v1.json b/wai/wai-quantum-cli/schemas/local-tools.v1.json index ffae06c45..72d1762ce 100644 --- a/wai/wai-quantum-cli/schemas/local-tools.v1.json +++ b/wai/wai-quantum-cli/schemas/local-tools.v1.json @@ -15,7 +15,8 @@ "signature", "key", "outcome", - "problems" + "problems", + "notes" ], "additionalProperties": false, "properties": { @@ -45,7 +46,14 @@ "items": { "type": "string" }, - "description": "What makes the job other than one the profile writes; empty when it is one." + "description": "What makes the job other than one the profile writes, from what is sealed; empty when it is one." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Where the host's unsealed words in the body (its status, its method, members nothing seals) differ from what is sealed: beside the verdict, and no part of it." } } }, diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 6a0f01f2d..3597a1fc2 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -776,6 +776,37 @@ fn at_inputs(text: String, inputs: &wai_quantum::qasm::Inputs) -> Result) -> Result { + use std::io::Read as _; + let past = |what: &str| format!("{what} holds more than the {MAX_JOB_BYTES} bytes a job takes"); + let bytes = match file { + Some(p) if p != Path::new("-") => { + let f = std::fs::File::open(p).map_err(|e| format!("{}: {e}", p.display()))?; + if f.metadata().map_err(|e| format!("{}: {e}", p.display()))?.len() > MAX_JOB_BYTES { + return Err(past(&p.display().to_string())); + } + let mut b = Vec::new(); + f.take(MAX_JOB_BYTES + 1).read_to_end(&mut b).map_err(|e| format!("{}: {e}", p.display()))?; + b + } + _ => { + let mut b = Vec::new(); + std::io::stdin().take(MAX_JOB_BYTES + 1).read_to_end(&mut b).map_err(|e| format!("standard input: {e}"))?; + b + } + }; + if bytes.len() as u64 > MAX_JOB_BYTES { + return Err(past("the job")); + } + String::from_utf8(bytes).map_err(|_| "the job is not UTF-8".to_string()) +} + 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())), @@ -1454,7 +1485,7 @@ fn main() { #[cfg(feature = "service")] Command::Task { cmd, args } => task::main(cmd, &args), Command::Verify { file, history: hist, bundle } => { - let src = read_text(file.as_deref()).unwrap_or_else(|e| die(&e)); + let src = read_job_text(file.as_deref()).unwrap_or_else(|e| die(&e)); let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}"))); #[cfg(feature = "wasmq")] if v["result"]["format"] == wai_wasmq::run::FORMAT { diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 7d3b04ea7..7f56852b6 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -632,7 +632,7 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli #[cfg(feature = "wasmq")] if given["result"]["format"] == wai_wasmq::run::FORMAT { let v = wai_wasmq::verify(given, None, wai_wasmq::run::EngineKind::default_here())?; - return Ok(json!({ "valid": v.ok(), "signature": v.signature, "key": v.signer, "outcome": v.outcome, "problems": v.well_formed })); + return Ok(json!({ "valid": v.ok(), "signature": v.signature, "key": v.signer, "outcome": v.outcome, "problems": v.well_formed, "notes": v.notes })); } let (valid, key) = job::verify(given)?; Ok(json!({ "valid": valid, "key": key })) @@ -1210,12 +1210,26 @@ mod tests { 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"); - // A run that faulted, its body edited to say it was done: the seal holds, the job does not. + // 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}"); + assert_eq!(v["structuredContent"]["signature"], true, "{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"], false, "{v}"); - assert_eq!(v["structuredContent"]["signature"], true, "{v}"); + assert_eq!(v["structuredContent"]["valid"], true, "{v}"); + assert_eq!(v["structuredContent"]["notes"][0], r#"the host's unsealed status says "done"; the sealed result says "fault""#, "{v}"); } #[test] diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index 3fe653e5f..b16c770be 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -315,11 +315,15 @@ pub fn verify(text: &str, bundle: Option<&Path>) -> bool { (None, _) => println!("outcome not stated"), } if verdict.well_formed.is_empty() { - println!("result a job the profile writes: its body agrees with what is signed, fuel within its budgets, a fault it names"); + 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 &verdict.well_formed { 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 &verdict.notes { + println!("note {n}"); + } match verdict.bundle { Some(true) => println!("bundle the envelope the receipt binds"), Some(false) => println!("bundle NOT the envelope the receipt binds"), diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index d8fe530ee..80f87559c 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -143,7 +143,7 @@ const IDENTITY: &[u8] = &[ ]; #[test] -fn verify_names_the_signer_and_the_outcome_and_refuses_an_edit_no_signature_covers() { +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"); @@ -169,13 +169,17 @@ fn verify_names_the_signer_and_the_outcome_and_refuses_an_edit_no_signature_cove signers.push(signer); } assert_ne!(signers[0], signers[1], "two keys, one signer line"); - // The body's status is not under the signature: an edit of it must not verify. + // 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()]); - assert!(!v.status.success(), "a job whose status was edited verified:\n{}", text(&v)); + 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] @@ -365,3 +369,20 @@ fn a_job_is_read_from_its_text_strictly_before_it_is_checked() { let o = run(&d, &["verify", j]); assert!(!o.status.success(), "a number written with a fraction: {}", text(&o)); } + +// On Linux, where a sparse file costs nothing: a job file past any job's size is refused by +// its size, before it is read whole and before its kind is known. +#[cfg(target_os = "linux")] +#[test] +fn verify_refuses_a_file_past_any_job_s_size_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 + } + // Refused for the file's length, which names it: not read, then found too long. + let o = run(&d, &["verify", path.to_str().unwrap()]); + assert!(!o.status.success() && text(&o).contains(&format!("{} holds more than", path.display())), "{}", text(&o)); +} diff --git a/wai/wai-wasmq/mutations.py b/wai/wai-wasmq/mutations.py index 5b6ce3806..3a510229b 100644 --- a/wai/wai-wasmq/mutations.py +++ b/wai/wai-wasmq/mutations.py @@ -92,7 +92,8 @@ M = [ ("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 body's status", WASMQ, "src/run.rs", ' "status" => out.push(format!("the body\'s status is {value}, where its signed result says {}", body_status(r))),', ' "status" => {}', ["--test", "review"], ["a_job_whose_status_or_method"]), + ("the host's unsealed status, noted", WASMQ, "src/run.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: key.as_str().map(str::to_string)", "signer: None", ["--test", "review"], ["a_job_names_its_signer"]), ("the seal is checked only in its form", WASMQ, "src/run.rs", " let signature = seal_form.is_empty() && {", " let signature = {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), ("the receipt's algorithm", WASMQ, "src/run.rs", ' if seal["alg"] != "ed25519" || seal["canon"] != "rfc8785" {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), diff --git a/wai/wai-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs index dcca0431f..853237f71 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -24,7 +24,7 @@ pub const RESULT: [&str; 16] = ["format", "profile", "status", "method", "qubits /// What a result's `energy` says (§6.4): no meter reads a run. pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; /// The members of a job body (§6.4). Only `request` and `result` are under the signature; -/// `status` and `method` repeat what the result says, and [`verify`] holds them to it. +/// `status` and `method` repeat what the result says, and [`verify`] notes where they do not. pub const BODY: [&str; 4] = ["status", "method", "request", "result"]; /// The fault reasons a receipt may name (§6.2). pub const REASONS: [&str; 11] = [ @@ -786,28 +786,36 @@ pub fn read_job(text: &str) -> Result { serde_json::from_str::(text).map(|s| s.0).map_err(|e| e.to_string()) } +/// 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. +fn unsealed(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")); + } + } + out +} + /// What makes a job other than one this profile writes (§6.4, §6.5). A signature shows /// that a key sealed a job, and says nothing of what the job holds: so without the /// envelope to run again, this is what holds a result to its contract and to itself. fn form(job: &Value) -> Vec { let mut out = Vec::new(); let (req, r) = (&job["request"], &job["result"]); - // The body: nothing in it but what the signature covers, or repeats from under it. - 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 body's status is {value}, where its signed result says {}", body_status(r))), - "method" if value == &r["method"] => {} - "method" => out.push(format!("the body's method is {value}, where its signed result says {}", r["method"])), - other => out.push(format!("the body holds `{other}`, which no signature covers")), - } - } - for name in BODY { - if job.get(name).is_none() { - out.push(format!("the body has no `{name}`")); - } - } // A number a double cannot hold would be read differently by readers of the receipt. for (what, value) in [("request", req), ("result", r)] { if let Some(at) = wai_quantum::engine::receipt::inexact_number(value) { @@ -989,8 +997,13 @@ pub struct Verdict { 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. + /// 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, @@ -1003,10 +1016,11 @@ impl Verdict { } } -/// Checks a wasmQ job body: its signature and who signed; that every member of the body -/// is one the signature covers or agrees with what it covers; that its result is one this -/// profile writes (fuel within budgets, a fault reason it names); and, given the bundle, -/// that it is the bundle the receipt binds and that running it again reproduces the result. +/// Checks a wasmQ job body by what is sealed: its signature and who signed; that its +/// request and result are ones this profile writes (fuel within budgets, a fault reason it +/// names); 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 { let (Some(request), Some(r)) = (job.get("request"), job.get("result")) else { return Err("expected a job: {\"request\": ..., \"result\": ...}, the result carrying its receipt".into()); @@ -1025,7 +1039,7 @@ pub fn verify(job: &Value, bundle: Option<&Opened>, kind: EngineKind) -> Result< }; let key = seal["key"].clone(); let c = &job["request"]["wasmq"]; - let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), well_formed: form(job), ..Verdict::default() }; + let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), well_formed: form(job), notes: unsealed(job), ..Verdict::default() }; if let Some(o) = bundle { let same = o.envelope_blake3 == job["request"]["envelope_blake3"] && &o.contract == c; v.bundle = Some(same); diff --git a/wai/wai-wasmq/tests/fuzz.rs b/wai/wai-wasmq/tests/fuzz.rs index aa7bc3abf..65f2a964c 100644 --- a/wai/wai-wasmq/tests/fuzz.rs +++ b/wai/wai-wasmq/tests/fuzz.rs @@ -322,9 +322,15 @@ fn a_changed_job_does_not_hold_with_its_envelope_and_no_job_or_record_panics() { if e == job { continue; } - // As edited: no signature, no form and no run lets a change through. + // 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}")); - assert!(!v.as_ref().is_ok_and(|v| v.ok()), "{file}: a changed job holds with its envelope: {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. diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs index f608224a9..92d3f5ffe 100644 --- a/wai/wai-wasmq/tests/review.rs +++ b/wai/wai-wasmq/tests/review.rs @@ -181,20 +181,37 @@ const IDENTITY: &str = r#"(module (func (export "f") (param i64) (result i64) lo const SPIN: &str = r#"(module (func (export "f") (param i64) (result i64) loop br 0 end i64.const 0))"#; #[test] -fn a_job_whose_status_or_method_was_edited_outside_the_signature_does_not_verify() { +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"); - assert!(wai_wasmq::verify(&fault, Some(&o), EngineKind::Interpreter).unwrap().ok(), "the positive control"); + 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"); - assert!(!wai_wasmq::verify(&edited, Some(&o), EngineKind::Interpreter).unwrap().ok(), "a fault edited to 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) in [("status", json!("fault")), ("method", json!("statevector")), ("note", json!("a member nothing signs"))] { + 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; - assert!(!wai_wasmq::verify(&edited, Some(&o), EngineKind::Interpreter).unwrap().ok(), "the body's `{member}` changed"); - assert!(!wai_wasmq::verify(&edited, None, EngineKind::Interpreter).unwrap().ok(), "the body's `{member}` changed, without the bundle"); + 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] -- 2.54.0 (Apple Git-157) From 653f22af0c604e9bd59a8dc99c3493e1263988d6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 07:41:40 -0500 Subject: [PATCH 08/17] wasmQ on the one verify path: one verdict on every surface, and wasmQ receipts in the corpus A wasmQ job is checked where every job is (engine::job::check): the engine holds it to the profile's form (engine::wasmq, quantum-wasmq section 6.5, now nineteen numbered rules), and the provider protocol's section 8.4 gains the reason wasmq_form. So `wai-quantum verify` and its --json and --lines, the service, both MCP servers, the Python package, the desktop app, a view and a page give a wasmQ job one verdict, its text read as section 8.2 reads every job's. Beside the verdict, `wasmq` states the outcome, each rule broken, and the host's unsealed words, which change no verdict. - The command line no longer dispatches a wasmQ job before reading it: the UTF-8 check, --lines, text_refusal and --json apply as to every job; --history is refused for one, with or without --json; --bundle adds `bundle` and `reproduced` to the JSON verdict, and compares the contract in its canonical form (4.0 is 4). - The MCP tool's wasmQ branch, and its schema branch, are gone: job::verdict answers. - wai-wasmq's verify is a view of the engine's check, and the runner takes the profile's constants from the engine, so a run writes what a verifier holds it to. - The receipts corpus holds six wasmQ jobs the command line sealed and fourteen crafted from them; the second verifier gains wasmq_form, written in Python from section 6.5 without the reference, and a plant (wasmq-unchecked) the differential catches. Each change has a test that fails on the merge commit: the engine's wasmq_verdict, the command line's verify and wasmq tests, the view's, the page build's, the desktop app's, the Python package's corpus test, wai-wasmq's respelt bundle, and the differential. Co-Authored-By: Claude Opus 5.5 --- .../verifiers/quantum-receipts.sh | 4 +- .github/workflows/wai-quantum-cli.yml | 4 +- ...26-11-05-wasmq-hybrid-quantum-classical.md | 26 +- wai/extensions/quantum-providers.md | 7 + wai/extensions/quantum-wasmq.md | 156 +++-- wai/quantum-conformance/receipts/README.md | 7 +- wai/quantum-conformance/receipts/generate.py | 53 ++ .../receipts/jobs/cli-wasmq-fault-init.json | 1 + .../receipts/jobs/cli-wasmq-fault-prior.json | 1 + .../receipts/jobs/cli-wasmq-fault-run.json | 1 + .../receipts/jobs/cli-wasmq-fault.json | 1 + .../receipts/jobs/cli-wasmq-run.json | 1 + .../receipts/jobs/cli-wasmq-state-job.json | 1 + .../jobs/crafted-wasmq-contract-method.json | 110 ++++ .../jobs/crafted-wasmq-counts-short.json | 110 ++++ .../jobs/crafted-wasmq-envelope-changed.json | 99 +++ .../jobs/crafted-wasmq-fault-unnamed.json | 98 +++ .../jobs/crafted-wasmq-first-form.json | 109 ++++ .../crafted-wasmq-fuel-past-job-budget.json | 98 +++ .../jobs/crafted-wasmq-fuel-unaccounted.json | 110 ++++ .../crafted-wasmq-instances-past-budget.json | 112 ++++ .../jobs/crafted-wasmq-modules-other.json | 110 ++++ .../crafted-wasmq-ok-before-all-shots.json | 110 ++++ .../receipts/jobs/crafted-wasmq-respelt.json | 110 ++++ .../crafted-wasmq-result-member-added.json | 111 ++++ .../jobs/crafted-wasmq-seed-past-exact.json | 110 ++++ .../jobs/crafted-wasmq-transcript-upper.json | 110 ++++ .../receipts/made.lock.json | 6 + wai/quantum-conformance/receipts/make.sh | 4 + .../receipts/manifest.json | 144 +++++ .../tools/receipt_verify.py | 607 +++++++++++++++++- wai/wai-quantum-cli/Cargo.lock | 1 - wai/wai-quantum-cli/README.md | 7 +- wai/wai-quantum-cli/docs/llms-full.txt | 137 +++- wai/wai-quantum-cli/docs/local-mcp.json | 50 +- wai/wai-quantum-cli/docs/service-mcp.json | 35 +- .../schemas/local-tools.v1.json | 96 ++- wai/wai-quantum-cli/src/main.rs | 34 +- wai/wai-quantum-cli/src/mcp.rs | 16 +- wai/wai-quantum-cli/src/wasmq.rs | 77 ++- wai/wai-quantum-cli/tests/verify.rs | 45 ++ wai/wai-quantum-cli/tests/wasmq.rs | 58 +- wai/wai-quantum-desktop/src/main.rs | 4 + .../schemas/service-tools.v1.json | 35 +- wai/wai-quantum-views/src/lib.rs | 14 + wai/wai-quantum/Cargo.toml | 2 +- wai/wai-quantum/src/engine/job.rs | 25 +- wai/wai-quantum/src/engine/mod.rs | 1 + wai/wai-quantum/src/engine/wasmq.rs | 495 ++++++++++++++ wai/wai-quantum/tests/wasmq_verdict.rs | 72 +++ wai/wai-wasmq/Cargo.lock | 1 - wai/wai-wasmq/Cargo.toml | 2 - wai/wai-wasmq/README.md | 4 +- wai/wai-wasmq/mutations.py | 50 +- wai/wai-wasmq/src/envelope.rs | 27 +- wai/wai-wasmq/src/profile.rs | 5 +- wai/wai-wasmq/src/run.rs | 486 +------------- wai/wai-wasmq/tests/review.rs | 15 + wai/wai-web/src/quantum_engine.rs | 4 + 59 files changed, 3640 insertions(+), 689 deletions(-) create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-init.json create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-prior.json create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-run.json create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-fault.json create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-run.json create mode 100644 wai/quantum-conformance/receipts/jobs/cli-wasmq-state-job.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-method.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-counts-short.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-envelope-changed.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-form.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-past-job-budget.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-unaccounted.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-instances-past-budget.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-modules-other.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-ok-before-all-shots.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-respelt.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-member-added.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-seed-past-exact.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-transcript-upper.json create mode 100644 wai/wai-quantum/src/engine/wasmq.rs create mode 100644 wai/wai-quantum/tests/wasmq_verdict.rs diff --git a/.github/wai-conformance/verifiers/quantum-receipts.sh b/.github/wai-conformance/verifiers/quantum-receipts.sh index 7c90642c2..899fd21e0 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: . # # Job receipts (wai.quantum.provider/1 section 8, wai/quantum-conformance/receipts/): the @@ -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 python3 -I wai/quantum-conformance/tools/receipt_differential.py --second-only -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 50609a7f3..05bcb8a03 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -35,6 +35,7 @@ on: - 'wai/quantum-conformance/tools/receipt_differential.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' pull_request: @@ -53,6 +54,7 @@ on: - 'wai/quantum-conformance/tools/receipt_differential.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' @@ -173,7 +175,7 @@ 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 # The release binary, pinned by version and SHA-256, rather than a build from source. diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index 80f55bcc1..c88a7c6e5 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -49,13 +49,27 @@ 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 reads the job's text strictly, names the signer and - how the run ended, holds the contract, the result and the receipt to the forms the text gives - under whichever key, notes beside its verdict where the host's unsealed words differ from what - is sealed, and runs the envelope again to compare byte for byte; the MCP server's - `verify_quantum_result` gives the same verdict. 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 + `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 nineteen 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 fourteen crafted from them, twelve breaking one rule each 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. - **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 diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index cb2ab7d42..964879c9a 100644 --- a/wai/extensions/quantum-providers.md +++ b/wai/extensions/quantum-providers.md @@ -556,6 +556,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 +599,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 diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index e5092662c..ceea276b0 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -5,7 +5,8 @@ the prepared module, `src/meter.rs`; the interpreter, `src/interp.rs`; the engin `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`). Interface: [`quantum-wasmq.wit`](quantum-wasmq.wit). +`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. @@ -574,9 +575,10 @@ 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; `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). +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: @@ -603,46 +605,112 @@ the receipt's digest is the same everywhere, and under one key the whole job is. ### 6.5 Verifying -`wai-quantum verify JOB.json` reads the job from its text strictly: a member twice, or a -number that is not a whole number of at most 2^53 − 1 in size written as one, is refused, -since readers differ on such text. It then checks, and exits 0 only when all hold: +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, the +Python package, the desktop app, a view and a page. The job is read from its text as §8.2 +reads every job's, so its seal and its numbers are held as every receipt's are. 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. -- the receipt's seal: a receipt of Ed25519 over the RFC 8785 form, its key, digest and - signature in lowercase hexadecimal, that seals this request and this result; it names the - **signer**, the key the receipt carries, as a `did:key`. A receipt shows that this key - sealed this request and this result; it does not say who holds the key, which is the - reader's to compare with the signer it expects; -- that the request and the result hold no whole number past 2^53 − 1; -- that the contract is one §2.2 gives: its members and no other, each of its form and - range, the method this version has, every module named by a callout; -- that the request and the result are ones this profile writes, under whichever key: - exactly the members §6.4 and §6.2 give, each of its form; the format, the profile and - the unmetered energy §6.4 gives; the method, shots, seed, budgets and modules the - contract gives, and no callout it does not name; fuel within its budgets that is what the - instances and the calls add up to, no more for the instances than the instances made - could be given, no more for a callout than its calls of its largest, and a fault's no - more than its phase is given; callouts that are the calls the fuel counts; a run - that is `ok` only after all its shots, or a fault in the shot after those that finished, - of a reason, a phase and a budget §6.2 names; and counts of whole outcomes of one width, - in bits, that sum to the shots run. +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 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: -Without the envelope this is all a check can hold a result to: a key can seal any result -of this form. Only running the envelope again shows that a result is the one it gives. +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 from 1 to 2^19, and `priors` an array each of whose priors has exactly the members + `name` (a string), `blake3` (a digest) and `bytes` (a count of at most 2^27). The + modules' `bytes` that are counts add up to at most 2^19. +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 + of at most 2^39 (§4.8). +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 of at most 2^15, 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 of at most `calls` × `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 of at most `F`'s `used` and of at most its phase's budget (the budget + `per_call` in a call, `per_instance` otherwise, 0 when that is not a count); it has a `callout` exactly + when its phase is `call`, and that is the `extern` of one of `C`'s callouts; 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`. -The verdict is the sealed content's alone, so that every surface that checks a job gives -the same one. The body's unsealed `status` and `method`, and any other member nothing -seals, are the host's word: where they differ from what is sealed, or are missing, a check -says so beside the verdict ("the host's unsealed status says `done`; the sealed result says -`fault`"), and the verdict is unchanged. The outcome it reports is the sealed result's. -`verify` reads at most 1 GiB, more than any job holds, and refuses a file past it by its -size, before it reads it. +Beside §8.4's reasons a verifier of the engine states, for a wasmQ job, `wasmq`: +`outcome`, the sealed result's `status`; `problems`, each rule the job breaks, in words, +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. -It prints how the run ended. With `--bundle ENVELOPE.wai` it also checks that the -envelope is the one the receipt binds, and runs it again here: the result must be the -same, byte for byte. A wasmQ job carries no history, so `--history` is refused for one. -The local MCP server's `verify_quantum_result` gives the same verdict. A verifier of the -engine's job receipts in general (the job service's, a view's) checks the seal alone, -which is the receipt's own claim, and not this section. +`wai-quantum verify JOB.json` prints the receipt's check, the outcome and each problem and +note; `--json` gives the verdict as JSON, and `--lines` one verdict a line, each a job's +whatever its kind. `verify` reads at most 1 GiB, more than any job holds, and refuses a +file past it by its size, before it reads it. 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 @@ -719,6 +787,14 @@ feasible fills the target, and atoms are moved, never made or lost. 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 breaking one rule of §6.5, so the two verifiers of +that corpus, which share no code, are held to each other on `wasmq_form`, 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. diff --git a/wai/quantum-conformance/receipts/README.md b/wai/quantum-conformance/receipts/README.md index 605f35a8e..6936c8afd 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`. diff --git a/wai/quantum-conformance/receipts/generate.py b/wai/quantum-conformance/receipts/generate.py index 3d231398a..402d1661a 100644 --- a/wai/quantum-conformance/receipts/generate.py +++ b/wai/quantum-conformance/receipts/generate.py @@ -700,6 +700,53 @@ def cases(): add("method-measured", edited(run, at(R + ("accuracy", "class"), "measured")), ["provider_class"], "the engine's own result labelled measured") add("method-simulated", edited(run, at(R + ("accuracy", "class"), "simulated")), ["provider_class"], "the engine's own result labelled simulated") add("method-class-unknown", edited(run, at(R + ("accuracy", "class"), "quasi-exact")), [], "a class a verifier does not know: not a reason, since a later version may add one") + + # --- a wasmQ job (quantum-wasmq section 6.5): each sealed again, one rule broken + wrun = load("cli-wasmq-run.json") + wfault = load("cli-wasmq-fault.json") + wjob = load("cli-wasmq-state-job.json") + C = ("request", "wasmq") + + def fewer(k, by=1): + def f(b): + counts = b["result"]["counts"] + most = max(counts, key=lambda o: counts[o]) + counts[most] -= by + if k: + b["result"]["shots_run"] -= by + return f + + def both(name, value): + def f(b): + b["request"]["wasmq"]["run"][name] = value + b["result"][name] = value + return f + + def budget(name, value): + def f(b): + b["request"]["wasmq"]["run"]["fuel"][name] = value + b["result"]["fuel"]["budget"][name] = value + return f + + add("wasmq-counts-short", edited(wrun, fewer(False)), ["wasmq_form"], "a wasmQ run whose counts add up to one shot fewer than it ran (rule 19)") + add("wasmq-ok-before-all-shots", edited(wrun, fewer(True)), ["wasmq_form"], "a wasmQ run that is ok after 31 of its 32 shots (rule 18)") + add("wasmq-fuel-unaccounted", edited(wrun, lambda b: b["result"]["fuel"].update(instances=b["result"]["fuel"]["instances"] + 1)), ["wasmq_form"], "a wasmQ run whose instances and calls do not add up to the fuel it used (rule 15)") + add("wasmq-fuel-past-job-budget", edited(wfault, budget("per_job", 100)), ["wasmq_form"], "a wasmQ fault that used 400 fuel under a run's budget of 100 (rule 13)") + add("wasmq-instances-past-budget", edited(wjob, budget("per_instance", 8000)), ["wasmq_form"], "one instance for the whole run, given more fuel for its seed, init and prior than three stretches of 8000 (rule 17)") + add("wasmq-fault-unnamed", edited(wfault, at(R + ("fault", "reason"), "out_of_fuel")), ["wasmq_form"], "a wasmQ fault of a reason the profile does not name (rule 18)") + add("wasmq-result-member-added", edited(wrun, at(R + ("note",), "a member the profile does not write")), ["wasmq_form"], "a wasmQ result with a member the profile does not write (rule 9)") + add("wasmq-contract-method", edited(wrun, both("method", "statevector")), ["wasmq_form"], "a wasmQ contract and result of a method the profile does not have (rule 8)") + add("wasmq-seed-past-exact", edited(wrun, both("seed", 2 ** 53)), ["wasmq_form"], "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)") + add("wasmq-modules-other", edited(wrun, lambda b: b["result"]["modules"][0].update(blake3="ab" * 32)), ["wasmq_form"], "a wasmQ result naming a module the contract does not pin (rule 16)") + add("wasmq-transcript-upper", edited(wrun, lambda b: b["result"].update(transcript_blake3=b["result"]["transcript_blake3"].upper())), ["wasmq_form"], "a wasmQ transcript digest in upper case (rule 12)") + add("wasmq-first-form", seal(wrun, form="first"), ["wasmq_form"], "a wasmQ job sealed in the first form, older than the profile: the seal holds, and no host wrote it (rule 10)") + respelt = copy.deepcopy(wrun) + respelt["request"]["wasmq"]["run"]["shots"] = Raw("32.0", 32) + respelt["result"]["shots"] = Raw("3.2e1", 32) + add("wasmq-respelt", (dump(respelt) + "\n").replace('"state": ', '"\\u0073tate": ', 1), [], "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") + moved = copy.deepcopy(wfault) + moved.update(status="done", method="statevector", id="j-another") + add("wasmq-envelope-changed", moved, [], "a wasmQ fault whose unsealed body says done, another method and an id: a note beside the verdict, not a reason") return out @@ -729,6 +776,12 @@ MADE = { "cli-set-sweep.json": ("the command line: a sweep of an input at three points, sealed as a set", []), "first-form-service-run.json": ("the job service as it was on 18 October 2026, commit 72d18742d, sealing with the first form's own writer: counts", []), "first-form-service-observables.json": ("the same service, the same writer: expectation values, doubles in its own text", []), + "cli-wasmq-run.json": ("the command line: a wasmQ run, a surface-code round decoded by WebAssembly between its gates, 32 shots", []), + "cli-wasmq-state-job.json": ("the command line: a wasmQ run of one instance for the whole run, its decoder reading a prior", []), + "cli-wasmq-fault.json": ("the command line: a wasmQ run that stopped in a call past the call's fuel budget", []), + "cli-wasmq-fault-run.json": ("the command line: a wasmQ run that stopped in its third shot when the run's fuel ran out", []), + "cli-wasmq-fault-init.json": ("the command line: a wasmQ run that stopped in an instance's init", []), + "cli-wasmq-fault-prior.json": ("the command line: a wasmQ run that stopped while a module read its prior", []), } diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-init.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-init.json new file mode 100644 index 000000000..c70485cd7 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-init.json @@ -0,0 +1 @@ +{"status":"fault","method":"stabilizer","request":{"wasmq":{"callouts":[{"export":"f","extern":"f","module":0}],"modules":[{"blake3":"f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3","bytes":50,"name":"m","priors":[]}],"profile":"wai.quantum.wasmq/1","program":{"blake3":"3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319","bytes":128,"inputs":{}},"run":{"fuel":{"per_call":10000000,"per_instance":100000000,"per_job":17179869184},"method":"stabilizer","seed":1,"shots":4,"state":"shot"}},"envelope_blake3":"d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9"},"result":{"format":"wai.quantum.wasmq.result/1","profile":"wai.quantum.wasmq/1","status":"fault","method":"stabilizer","qubits":8,"shots":4,"shots_run":0,"seed":1,"counts":{},"callouts":0,"fuel":{"used":1,"instances":1,"budget":{"per_call":10000000,"per_job":17179869184,"per_instance":100000000},"callouts":{}},"modules":[{"name":"m","blake3":"f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3"}],"transcript_blake3":"1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881","accuracy":{"class":"exact"},"fault":{"reason":"unreachable","shot":0,"phase":"init","module":"m","fuel":1},"energy":{"unmetered":"the runner reads no energy meter; fuel is the profile's measure of work"},"receipt":{"alg":"ed25519","canon":"rfc8785","key":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"ac633eb1fb15f62ad6df9b6b7891dc7ada2dac6c1503e5f4d073dc866b40cd6d","signature":"0c6cdfa74c73a0922c550acef28da9cca21f87eae4310ed81e7365dc85a6076361b11316c21bc0a2db66f103d0f7c2e7c46bd4da68620c1112d3ac80a728c109"}}} diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-prior.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-prior.json new file mode 100644 index 000000000..bbb2f78c0 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-prior.json @@ -0,0 +1 @@ +{"status":"fault","method":"stabilizer","request":{"wasmq":{"callouts":[{"export":"f","extern":"f","module":0}],"modules":[{"blake3":"2bd6df6f77cff04a53312cc69ee9024ae6e445af9dd3a7d410cc5c8a18c0c4d0","bytes":105,"name":"m","priors":[{"blake3":"024f67c0425a3dc02fbaf58cb93de5132e3d75c519faa0bada21491d88c97057","bytes":5,"name":"weights"}]}],"profile":"wai.quantum.wasmq/1","program":{"blake3":"3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319","bytes":128,"inputs":{}},"run":{"fuel":{"per_call":10000000,"per_instance":100000000,"per_job":17179869184},"method":"stabilizer","seed":1,"shots":4,"state":"shot"}},"envelope_blake3":"6da2c86da1b3d288df324fa7902e4cb10c9c5b21160921907299402466c5d50c"},"result":{"format":"wai.quantum.wasmq.result/1","profile":"wai.quantum.wasmq/1","status":"fault","method":"stabilizer","qubits":8,"shots":4,"shots_run":0,"seed":1,"counts":{},"callouts":0,"fuel":{"used":8,"instances":8,"budget":{"per_call":10000000,"per_job":17179869184,"per_instance":100000000},"callouts":{}},"modules":[{"name":"m","blake3":"2bd6df6f77cff04a53312cc69ee9024ae6e445af9dd3a7d410cc5c8a18c0c4d0"}],"transcript_blake3":"3db8f511346d060c257940c086300666c8eb1b27c6083d36066b8d8f38e6408c","accuracy":{"class":"exact"},"fault":{"reason":"unreachable","shot":0,"phase":"prior","module":"m","fuel":8},"energy":{"unmetered":"the runner reads no energy meter; fuel is the profile's measure of work"},"receipt":{"alg":"ed25519","canon":"rfc8785","key":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"48f6ca3e7cc12a0ac8f277c342bf8098c9ca9d2938f855eb75336a33e0ce29b3","signature":"78b5984d9ce1a89580c32cdd987840b89d357745d23e70ae259bdd66ae0cc128ca10cc0e754e67bf71e1c768c531e340f7b5ea9d654a2508d9fb6f60709ba80a"}}} diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-run.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-run.json new file mode 100644 index 000000000..7bc70be93 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault-run.json @@ -0,0 +1 @@ +{"status":"fault","method":"stabilizer","request":{"wasmq":{"callouts":[{"export":"f","extern":"f","module":0}],"modules":[{"blake3":"9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43","bytes":68,"name":"m","priors":[]}],"profile":"wai.quantum.wasmq/1","program":{"blake3":"3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319","bytes":128,"inputs":{}},"run":{"fuel":{"per_call":10000000,"per_instance":100000000,"per_job":50},"method":"stabilizer","seed":1,"shots":8,"state":"shot"}},"envelope_blake3":"0a19542a6a9178984a93d9863a1d5e9371e00b86c6e9143e04b5f0fb5b0be177"},"result":{"format":"wai.quantum.wasmq.result/1","profile":"wai.quantum.wasmq/1","status":"fault","method":"stabilizer","qubits":8,"shots":8,"shots_run":2,"seed":1,"counts":{"0101101101011011":1,"1011111010111110":1},"callouts":3,"fuel":{"used":50,"instances":0,"budget":{"per_call":10000000,"per_job":50,"per_instance":100000000},"callouts":{"f":{"calls":3,"fuel":50,"max":23}}},"modules":[{"name":"m","blake3":"9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43"}],"transcript_blake3":"3772395e9bb2f9fba3b1a7ef4f5def3e5f10f76a9ba517d16056e5fd7cb7d413","accuracy":{"class":"exact"},"fault":{"reason":"fuel_exhausted","shot":2,"phase":"call","module":"m","fuel":4,"callout":"f","budget":"run"},"energy":{"unmetered":"the runner reads no energy meter; fuel is the profile's measure of work"},"receipt":{"alg":"ed25519","canon":"rfc8785","key":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"39102ed51a3b76f36d8fafb5a7f4d9af7c1d2363e74b5c1d6dfe593770b364ad","signature":"7705ba3a31066100c6b9e228716e2359a834eb10b6bffda1bcca5da1483227ae0a4553e113b351e4ae4861060eb73b01343c0e26ec58f7c5bba7d730fc1dc70e"}}} diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault.json new file mode 100644 index 000000000..c3ef75f4b --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-fault.json @@ -0,0 +1 @@ +{"status":"fault","method":"stabilizer","request":{"wasmq":{"callouts":[{"export":"f","extern":"f","module":0}],"modules":[{"blake3":"a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1","bytes":57,"name":"m","priors":[]}],"profile":"wai.quantum.wasmq/1","program":{"blake3":"3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319","bytes":128,"inputs":{}},"run":{"fuel":{"per_call":400,"per_instance":100000000,"per_job":17179869184},"method":"stabilizer","seed":9,"shots":32,"state":"shot"}},"envelope_blake3":"22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35"},"result":{"format":"wai.quantum.wasmq.result/1","profile":"wai.quantum.wasmq/1","status":"fault","method":"stabilizer","qubits":8,"shots":32,"shots_run":0,"seed":9,"counts":{},"callouts":1,"fuel":{"used":400,"instances":0,"budget":{"per_call":400,"per_job":17179869184,"per_instance":100000000},"callouts":{"f":{"calls":1,"fuel":400,"max":400}}},"modules":[{"name":"m","blake3":"a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1"}],"transcript_blake3":"47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7","accuracy":{"class":"exact"},"fault":{"reason":"fuel_exhausted","shot":0,"phase":"call","module":"m","fuel":400,"callout":"f","budget":"call"},"energy":{"unmetered":"the runner reads no energy meter; fuel is the profile's measure of work"},"receipt":{"alg":"ed25519","canon":"rfc8785","key":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"9cab411ba32a647e5c4b0ce53609a4308c1a07bd26c817fca582e3e04258bff4","signature":"c939aab422b5bf97f2abb4264df90c7aa2c69287570ed177edaa1a2fad8c4ddc9ea3758ee11cea21341c84a9d4eb47da4549431f32c9d8adbfe2721ade007a07"}}} diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-run.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-run.json new file mode 100644 index 000000000..e6b41f001 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-run.json @@ -0,0 +1 @@ +{"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":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1","signature":"1ea3250661d07afe9e5f0bac876186cb6fb93ff2c9595e1f4d41c1bb7f9b0674d0146f9329a24d3b22c74d666f3f63329b19970c9ee90eb199f448e7a0e13a01"}}} diff --git a/wai/quantum-conformance/receipts/jobs/cli-wasmq-state-job.json b/wai/quantum-conformance/receipts/jobs/cli-wasmq-state-job.json new file mode 100644 index 000000000..80624356d --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/cli-wasmq-state-job.json @@ -0,0 +1 @@ +{"status":"done","method":"stabilizer","request":{"wasmq":{"callouts":[{"export":"decode64","extern":"decode64","module":0}],"modules":[{"blake3":"005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962","bytes":39442,"name":"qldpc-decoder","priors":[{"blake3":"a0a28225df247910039d0693ff2dfb69d3b012487e81fc449486dd5b9afc3fd7","bytes":231,"name":"checks"}]}],"profile":"wai.quantum.wasmq/1","program":{"blake3":"1c79196a4e25b4f73f53a8327d9ab5d4d1644040f6b5036bb34aa1d456f2ee28","bytes":3346,"inputs":{}},"run":{"fuel":{"per_call":10000000,"per_instance":100000000,"per_job":17179869184},"method":"stabilizer","seed":77,"shots":16,"state":"job"}},"envelope_blake3":"3691fd8cac6fffe58d46a496aeff248a40d654129fe8cd23bae3dd6d3cebf8f4"},"result":{"format":"wai.quantum.wasmq.result/1","profile":"wai.quantum.wasmq/1","status":"ok","method":"stabilizer","qubits":47,"shots":16,"shots_run":16,"seed":77,"counts":{"000000000000000000000000000000000000000000000000000000000000000000000000001001110011":1,"000000000000000000000000000000000000000000000000000000000000010000000000011110000000":1,"000000000000000000000000000000000000000000000000000001000000010000000010110100000000":1,"000000000000000000000000000000000000000000000000000010001000000101000010010010101001":1,"000000000000000000000000000000000000000000000000010000000100000100001001000110000100":1,"000000000000000000000000000000000000000000000000100000000100000100001010000110100100":1,"000000000000000000000000000000000000000000000001000000000000011000000010010000001110":1,"000000000000000000000000000000000000000000000010100000000000000010001110000110101111":1,"000000000000000000000000000000000000001000000000000000000000000000100000001111010111":1,"000000000000000000000001100000000000000000000010000000000000010010000100010111100001":1,"000000000000011000110000000000000000000000000000000000011000000001000001000101001011":1,"000000000000011000110000000000000000000000000000000100011000010000000101010101100000":1,"000000011000110000000000000000000000000000000100001000000000000000010001000110010001":1,"001010001100000000000000000000000000000001000010000000000000000000000100001000010110":1,"001100000000000000000000000000000000000010100000000000000000001000110000001001110110":1,"110000000000000000000000000000000000001000000000000001000000000000100010100100011000":1},"callouts":16,"fuel":{"used":765610,"instances":24078,"budget":{"per_call":10000000,"per_job":17179869184,"per_instance":100000000},"callouts":{"decode64":{"calls":16,"fuel":741532,"max":57659}}},"modules":[{"name":"qldpc-decoder","blake3":"005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962"}],"transcript_blake3":"eb5252076ea8bb4c45ccec714a3cc64b89e833fe0a49c36a28881b63b1c3799e","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":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"5109fd0c11be13f637530e8861ec2d625a2985a9a3ad9340e0d00d36aea2346d","signature":"62f2f505f3a36cf6e94e4113639db0eb64c4efe8ee8a902c4895e11f4de322ffdb6f610e68dbcbf6bdd2698ba7a0e46925a65134b95547d434070e6316cd1e09"}}} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-method.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-method.json new file mode 100644 index 000000000..32fbf2d93 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-method.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": "statevector", + "seed": 1234, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "ok", + "method": "statevector", + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "441385ba757f9a5db8c0299871049c51c39a6650212b33c47ea4622720bc9de2", + "signature": "afd53deb5667de62981865b46e768c59629055926c5c0351e5c02ad3062a3d2c560235bd83c0ef289d449c278c05de94f57c47d38b618744f9b214b9f5fb910c" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-counts-short.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-counts-short.json new file mode 100644 index 000000000..a5fbde8ee --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-counts-short.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": 3, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "992a20a6788ae3dade8cdfa90e798fed53867f2d05189cde9cf2a9d4b14706fa", + "signature": "65d3cab9ccdbd288f3819c9c5b46bc9e3259148a349a8d0780622b161564f28fac036d94a63d68ebdd72ee6c3f0f151dfa91fbe94f20f644cd2cfd4d5d7ebb0d" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-envelope-changed.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-envelope-changed.json new file mode 100644 index 000000000..5e58b1c40 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-envelope-changed.json @@ -0,0 +1,99 @@ +{ + "status": "done", + "method": "statevector", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 400, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 400, + "max": 400 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "9cab411ba32a647e5c4b0ce53609a4308c1a07bd26c817fca582e3e04258bff4", + "signature": "c939aab422b5bf97f2abb4264df90c7aa2c69287570ed177edaa1a2fad8c4ddc9ea3758ee11cea21341c84a9d4eb47da4549431f32c9d8adbfe2721ade007a07" + } + }, + "id": "j-another" +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json new file mode 100644 index 000000000..53e2583cb --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 400, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 400, + "max": 400 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "out_of_fuel", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "bd9c3117100fc749a7600c429496074a5104c9c98451b859236a3a2c126b93bb", + "signature": "49c9586b6875a005bf312823a8608205fa0d093da7eaa727596bd504146d7c8c3c362b00ac7798af68cc81f6c42707c057702e45110700ed5ea039cbb586a309" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-form.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-form.json new file mode 100644 index 000000000..c5e2525af --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-form.json @@ -0,0 +1,109 @@ +{ + "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", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "3b1d20813e1400bcb189647a563054d2b7af6c392b4dfc0a94e377d20600137d", + "signature": "327b8d73f295ed0a61f5c9352bc194c3a69746dd20f1af4d3aae650bac97076b8d188e9588f6a1dc844a26558e46c7ba18366e097b53ff9d985f2453f826610e" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-past-job-budget.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-past-job-budget.json new file mode 100644 index 000000000..38b7d875b --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-past-job-budget.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 100 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 400, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 100, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 400, + "max": 400 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "a52c41a705873742fc38fcbbd2e9c63d7e8afd3afd4a693001dceac368dfc330", + "signature": "90784ba730c4a7bf618b2193ec8f215330f65c45b047e5d2da16337a0f5e5c4cb3d3ee82963c429e61bfefb5b218c7d3ba76f322a6b5f169be6226b12906930a" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-unaccounted.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-unaccounted.json new file mode 100644 index 000000000..fc45bf533 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-unaccounted.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": 248033, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "f3ca37425506d722cbad2ee337cdfc0e8c894bc4b75091e680130de1a94a2223", + "signature": "3454dfda2f0409e93d6f07404750f54e2809648cc065ce54d676dac5de6a9583099cff05f0c9780b0f83361b61213a1f5187eec93725b3088df35088dcfc8803" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-instances-past-budget.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-instances-past-budget.json new file mode 100644 index 000000000..c09e2d078 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-instances-past-budget.json @@ -0,0 +1,112 @@ +{ + "status": "done", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "decode64", + "extern": "decode64", + "module": 0 + } + ], + "modules": [ + { + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962", + "bytes": 39442, + "name": "qldpc-decoder", + "priors": [ + { + "blake3": "a0a28225df247910039d0693ff2dfb69d3b012487e81fc449486dd5b9afc3fd7", + "bytes": 231, + "name": "checks" + } + ] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "1c79196a4e25b4f73f53a8327d9ab5d4d1644040f6b5036bb34aa1d456f2ee28", + "bytes": 3346, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 8000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 77, + "shots": 16, + "state": "job" + } + }, + "envelope_blake3": "3691fd8cac6fffe58d46a496aeff248a40d654129fe8cd23bae3dd6d3cebf8f4" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "ok", + "method": "stabilizer", + "qubits": 47, + "shots": 16, + "shots_run": 16, + "seed": 77, + "counts": { + "000000000000000000000000000000000000000000000000000000000000000000000000001001110011": 1, + "000000000000000000000000000000000000000000000000000000000000010000000000011110000000": 1, + "000000000000000000000000000000000000000000000000000001000000010000000010110100000000": 1, + "000000000000000000000000000000000000000000000000000010001000000101000010010010101001": 1, + "000000000000000000000000000000000000000000000000010000000100000100001001000110000100": 1, + "000000000000000000000000000000000000000000000000100000000100000100001010000110100100": 1, + "000000000000000000000000000000000000000000000001000000000000011000000010010000001110": 1, + "000000000000000000000000000000000000000000000010100000000000000010001110000110101111": 1, + "000000000000000000000000000000000000001000000000000000000000000000100000001111010111": 1, + "000000000000000000000001100000000000000000000010000000000000010010000100010111100001": 1, + "000000000000011000110000000000000000000000000000000000011000000001000001000101001011": 1, + "000000000000011000110000000000000000000000000000000100011000010000000101010101100000": 1, + "000000011000110000000000000000000000000000000100001000000000000000010001000110010001": 1, + "001010001100000000000000000000000000000001000010000000000000000000000100001000010110": 1, + "001100000000000000000000000000000000000010100000000000000000001000110000001001110110": 1, + "110000000000000000000000000000000000001000000000000001000000000000100010100100011000": 1 + }, + "callouts": 16, + "fuel": { + "used": 765610, + "instances": 24078, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 8000 + }, + "callouts": { + "decode64": { + "calls": 16, + "fuel": 741532, + "max": 57659 + } + } + }, + "modules": [ + { + "name": "qldpc-decoder", + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962" + } + ], + "transcript_blake3": "eb5252076ea8bb4c45ccec714a3cc64b89e833fe0a49c36a28881b63b1c3799e", + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "1c206e03bf6f101beb5edb5980d56dc39ef1fccc6eceef5f7bcd13b75b497c3b", + "signature": "e75ea79d367fddcca67ab5453a84b17dad3806846f038c14c58345cee4dfe73a461302a3c5e89c3216495e9d0f3ca6087c32b20b833e9917249c94c3af77050d" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-modules-other.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-modules-other.json new file mode 100644 index 000000000..55cb27dad --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-modules-other.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": "abababababababababababababababababababababababababababababababab" + } + ], + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "d7b11e29fe5bea4e68237efac286ff6c45b52f74118fb179895f91c0528e0f94", + "signature": "62cdcd744588c9a569420ad8d075cb579f89e50fd4ab41ea6889a90b4a5d01dec7205245684aafd409823ade469fede4f858436988c813526e80a56e8459d60d" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-ok-before-all-shots.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-ok-before-all-shots.json new file mode 100644 index 000000000..ecca3b77f --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-ok-before-all-shots.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": 31, + "seed": 1234, + "counts": { + "000000000000000000000000001001": 3, + "000000000000000000000000001010": 2, + "000000000000000000000000001011": 3, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "9a071896c902c5b761037264068dd5091628d88f908a91009ced24bce3cc6902", + "signature": "2be8d8084fa580eae554d50066c7a342ca51685c8836b511140a347484b796742c9ee52fdec69a295f4d089ada18af2d5210f02fd40b7a4c766183d83312410c" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-respelt.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-respelt.json new file mode 100644 index 000000000..c36df70ab --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-respelt.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.0, + "\u0073tate": "shot" + } + }, + "envelope_blake3": "3a11569a35aa1baad3963b0090c56755638390a8f843bbdf5e34f1d212352236" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "ok", + "method": "stabilizer", + "qubits": 17, + "shots": 3.2e1, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "df6d2e83f44afa7db6d7c6a3d4791443525b1778b794023326296050cb47dcb1", + "signature": "1ea3250661d07afe9e5f0bac876186cb6fb93ff2c9595e1f4d41c1bb7f9b0674d0146f9329a24d3b22c74d666f3f63329b19970c9ee90eb199f448e7a0e13a01" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-member-added.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-member-added.json new file mode 100644 index 000000000..866293e2c --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-member-added.json @@ -0,0 +1,111 @@ +{ + "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" + }, + "note": "a member the profile does not write", + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "593a9e2ccb96f59a8b95be3f8193085275245e40c3625923047dcbde34334531", + "signature": "71a8617693a99ad90f38e61edcd1dc1f04bfe8090adc19e07f5aa0436f83c63597e7702c2b158140963183b0715b46b0843343825ed93952ac1830ea986dfd0a" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-seed-past-exact.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-seed-past-exact.json new file mode 100644 index 000000000..d07c49b22 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-seed-past-exact.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": 9007199254740992, + "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": 9007199254740992, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "4db53dce118ecb83ec243bbce84c1ad5fc878cfb0e9ccc751b7bf0a1bdc79bc1", + "signature": "d4fc00c7c1f5eaf7139f1e17f1d317406b56aabb3869716ab466078a7104463a68ba46b8f14c12498e57aebc2e9fff7b2e24e45450752ebce499e3ab1968920b" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-transcript-upper.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-transcript-upper.json new file mode 100644 index 000000000..50bd1699b --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-transcript-upper.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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "630fd56edc1fbecb0936c3cfd47a2624b8fcc96a5dbae4c9226129f83a23e144", + "signature": "a2311d8ae5cc18d25a0012a737b783e0dabf1cd3a1c3bcd5552dfb28ea8450ee9d56fbdea688c274b186732457c521b1f103ecc9b80814803ccf301fbcfb1300" + } + } +} diff --git a/wai/quantum-conformance/receipts/made.lock.json b/wai/quantum-conformance/receipts/made.lock.json index 54e1afcd7..df79e81f7 100644 --- a/wai/quantum-conformance/receipts/made.lock.json +++ b/wai/quantum-conformance/receipts/made.lock.json @@ -8,6 +8,12 @@ "cli-run-trajectories.json": "c41c35bb5384d3582d83450f7b3d38225be2d353e4e3ad71e49a82c306656ede", "cli-set-programs.json": "81059f216b449468ac4ab6803de72a0a5f894458cb178eae15f2abf42bfd2437", "cli-set-sweep.json": "2b9af76474074a522ac5404c2de07a2b307620ce56e413b2319523b26fd168e1", + "cli-wasmq-fault-init.json": "be0c62291b21d35cdf10955dc3611913b6fdc824561d14ce509fecceeee98465", + "cli-wasmq-fault-prior.json": "47e497cf002051e090161a4eaa70933d46f2ecb56dbacdf05f83ecf74f67a065", + "cli-wasmq-fault-run.json": "00367bd6ff66f4d8f354dbb1f5c8e3519687924ab0e871800815a9d7e4943b8d", + "cli-wasmq-fault.json": "f86dc76d54591e765f4609adfe2e817e775421094c0b4d23284c54d9fc9a0122", + "cli-wasmq-run.json": "09c757a4739e448945cad383391d4c8cae140c678293a95af72d1d334cef214f", + "cli-wasmq-state-job.json": "cc2820d766d8b90f4e7fa3dc7b5d8528e500fc58555834ac870f83844f06b280", "first-form-service-observables.json": "098a720239f1757bb7ae7ceb8e35f7e0c30c91bb5fa7c977aef9aec02b855fa9", "first-form-service-run.json": "f21a23fd33df0ec4c540bbc341c4296aeb4612bc4b9e58044604cbd19521a535", "fixture-compile-legacy.json": "04c53ef2b350a4cdbd5f42deb8c44a91a1616a05c498a30f2f9cecd9c7402bca", diff --git a/wai/quantum-conformance/receipts/make.sh b/wai/quantum-conformance/receipts/make.sh index 50c14673b..8f744d402 100644 --- a/wai/quantum-conformance/receipts/make.sh +++ b/wai/quantum-conformance/receipts/make.sh @@ -11,6 +11,7 @@ # the ones it was given, and never makes them again to pass a test. set -euo pipefail B=$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1") +ENVELOPES=$(cd "$(dirname "$0")/../wasmq/envelopes" && pwd -P) OUT=$(mkdir -p "$2" && cd "$2" && pwd -P) W=$(mktemp -d) trap 'rm -rf "$W"' EXIT @@ -33,6 +34,9 @@ printf 'OPENQASM 3.0;\ninclude "stdgates.inc";\nqubit[1] q;\nrx(0.4) q[0];\n' > 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..3fe670321 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,114 @@ "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-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_verify.py b/wai/quantum-conformance/tools/receipt_verify.py index e6810d532..cf01b8226 100644 --- a/wai/quantum-conformance/tools/receipt_verify.py +++ b/wai/quantum-conformance/tools/receipt_verify.py @@ -2,9 +2,11 @@ """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 @@ -23,6 +25,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 +41,12 @@ 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 +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 [W1] to [W8]. """ import hashlib @@ -54,7 +59,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 +1039,300 @@ 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. + +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 = 19 +_ABSENT = object() # a member that is not there: 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): + 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) + # SPEC-GAP: [W1] "run's per_call", "per_job", "per_instance": run has no such member; + # read as its fuel's, the three budgets rule 8 names. + 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. + def prior_holds(p): + return (_exactly(p, ("name", "blake3", "bytes")) and isinstance(p["name"], str) + and _lower_hex(p["blake3"], 64) and _count_in(p["bytes"], 0, 2 ** 27)) + + def module_holds(m): + return (_exactly(m, ("name", "blake3", "bytes", "priors")) and isinstance(m["name"], str) + and _lower_hex(m["blake3"], 64) and _count_in(m["bytes"], 1, 2 ** 19) + 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 at most 2^39. + 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 all(_count_in(b, 0, 2 ** 39) for b in (per_call, per_job, per_instance))) + + # 9. r: exactly §6.4's members, and may have fault. + rule(9, set(r) - {"fault"} == WASMQ_RESULT_MEMBERS) + + # 10. The receipt's canon is rfc8785. + # SPEC-GAP: [W3] the receipt is not sealed, though §6.5 says only what is sealed + # decides; rule 10 names the receipt, and is read as written: 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 of at most 2^15; its transcript_blake3 a digest. + rule(12, _count_in(_get(r, "qubits"), 0, 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 at most calls × max. + # SPEC-GAP: [W4] "at most fuel": the member's own fuel, not F. + 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"]) + and _n(e["fuel"]) <= _n(e["calls"]) * _n(e["max"])) + + 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, otherwise shots_run, and 1 more when status is fault. + # SPEC-GAP: [W6] "and 1 more when r's status is fault" is read inside "otherwise": + # with state job one instance serves the run, a fault or not. + 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, used, modules, externs, + per_call, per_instance)) + else: + rule(18, False) # any other status breaks the form + + # 19. r's counts: an object of outcomes of 0 and 1, all of one length, each at least + # 1, adding up to shots_run. + # SPEC-GAP: [W7] an empty outcome holds only 0 and 1 (none), as §8.4's counts reads one. + 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)) + + return sorted(broken) + + +def _fault_holds(fault, shots, shots_run, used, modules, externs, per_call, per_instance): + """§6.5 rule 18, for a result whose status is fault.""" + required = {"reason", "shot", "phase", "module", "fuel"} + if not (isinstance(fault, dict) and required <= set(fault) <= required | {"callout", "budget"}): + return False + reason, phase = fault["reason"], fault["phase"] + in_call = _text_is(phase, "call") + # SPEC-GAP: [W8] a phase not among the four is "otherwise": per_instance. + phase_budget = per_call if in_call else per_instance + return (_text_is(reason, *WASMQ_FAULT_REASONS) + and _count(shots_run) and _count(shots) and _n(shots_run) < _n(shots) + and _count(fault["shot"]) and _n(fault["shot"]) == _n(shots_run) + and _text_is(phase, *WASMQ_PHASES) + and any(_same(fault["module"], _get(m, "name")) for m in modules) + and _count(fault["fuel"]) and _count(used) and _n(fault["fuel"]) <= _n(used) + and _n(fault["fuel"]) <= _or_zero(phase_budget) + # a callout exactly in a call, one of C's externs + and ("callout" in fault) == in_call + and (not in_call or any(_same(fault["callout"], e) for e in externs)) + # a budget exactly for fuel_exhausted: run, or call in a call and instance otherwise + and ("budget" in fault) == _text_is(reason, "fuel_exhausted") + and ("budget" not in fault + or _text_is(fault["budget"], "run", "call" if in_call else "instance"))) + + +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. + if "wasmq-unchecked" in PLANTS: # PLANT: skip the wasmQ rules + return + if is_wasmq(result) and wasmq_broken(request, result): + reasons.add("wasmq_form") + + +# ===================================================================== 8. a job def _verdict(reasons, key=None, form=None): return {"valid": not reasons, "reasons": sorted(reasons), "key": key, "form": form} @@ -1070,13 +1368,16 @@ 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. + check_wasmq(request, result, reasons) verdict = _verdict(reasons, key, form) if provider: verdict["provider"] = provider_statement(result) 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" @@ -1351,7 +1652,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"), @@ -2012,6 +2313,293 @@ 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: a wasmQ job made and sealed here, run to its end or stopped by a + # fault, and each rule broken alone (the job sealed again, so that its seal holds). + # Rule 1 cannot be broken alone: 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, so it is broken beside one. + def wasmq_parts(fault): + 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": 87}]}, + {"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": 6900, "instances": 400, "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, callouts={"decode64": {"calls": 3, "fuel": 1000, "max": 400}}) + return asked, made + + def wasmq_job(fault=False, request=None, result=None, envelope='"status":"done","method":"stabilizer",', + **seal_options): + """A wasmQ job, each change a path (members joined by "__") and its value.""" + 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(value) + else: + node[last] = value + return _seal(seed, _dump(asked), _dump(made), envelope=envelope, **seal_options) + + def rules_of(raw): + job = read_json(raw) + return wasmq_broken(job["request"], job["result"]) + + def changed_after(raw, change): + job = json.loads(raw.decode()) + change(job) + return json.dumps(job).encode() + + plan_fuel = {"calls": 2, "fuel": 100, "max": 60} + wasmq_cases = ( + # (name, job, the reasons, the rules of §6.5 it breaks, None for no wasmQ job) + ("wasmq: a run", wasmq_job(), [], []), + ("wasmq: a run stopped by a fault", wasmq_job(fault=True, envelope='"status":"fault","method":"stabilizer",'), + [], []), + ("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}), [], []), + ("wasmq: qubits 0", wasmq_job(result={"qubits": 0}), [], []), + ("wasmq: qubits 2^15", wasmq_job(result={"qubits": 2 ** 15}), [], []), + ("wasmq: instances and used moved together", wasmq_job(result={"fuel__instances": 401, "fuel__used": 6901}), + [], []), + ("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, result={ + "fault__phase": "init", "fault__budget": "instance", "fault__callout": _DELETE}), [], []), + ("wasmq: a fault of the run's budget in a call", wasmq_job(fault=True, result={"fault__budget": "run"}), + [], []), + ("wasmq: a trap in seed", wasmq_job(fault=True, result={ + "fault__reason": "unreachable", "fault__phase": "seed", "fault__callout": _DELETE, + "fault__budget": _DELETE}), [], []), + ("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: 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. + ("wasmq rule 1: an input of 2^53", wasmq_job(request={"wasmq__program__inputs__rounds": 2 ** 53}), + ["wasmq_form"], [1, 5]), + ("wasmq rule 1: an input of -2^53", wasmq_job(request={"wasmq__program__inputs__offset": -2 ** 53}), + ["wasmq_form"], [1, 5]), + ("wasmq rule 1: a seed of 2^53", wasmq_job(request={"wasmq__run__seed": 2 ** 53}, result={"seed": 2 ** 53}), + ["wasmq_form"], [1, 8]), + # Rules 2 to 19, each alone. + ("wasmq rule 2: envelope_blake3 in upper case", wasmq_job(request={"envelope_blake3": "E5" * 32}), + ["wasmq_form"], [2]), + ("wasmq rule 2: a member more", wasmq_job(request={"note": "x"}), ["wasmq_form"], [2]), + ("wasmq rule 2: envelope_blake3 missing", wasmq_job(request={"envelope_blake3": _DELETE}), + ["wasmq_form"], [2]), + ("wasmq rule 3: the program a member more", wasmq_job(request={"wasmq__program__note": "x"}), + ["wasmq_form"], [3]), + ("wasmq rule 3: the contract a member more", wasmq_job(request={"wasmq__note": "x"}), ["wasmq_form"], [3]), + ("wasmq rule 3: run a member more", wasmq_job(request={"wasmq__run__note": "x"}), ["wasmq_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}), ["wasmq_form"], [3]), + ("wasmq rule 4: another profile", wasmq_job(request={"wasmq__profile": "wai.quantum.wasmq/2"}), + ["wasmq_form"], [4]), + ("wasmq rule 5: no bytes", wasmq_job(request={"wasmq__program__bytes": 0}), ["wasmq_form"], [5]), + ("wasmq rule 5: past 2^20 bytes", wasmq_job(request={"wasmq__program__bytes": 2 ** 20 + 1}), + ["wasmq_form"], [5]), + ("wasmq rule 5: an input not whole", wasmq_job(request={"wasmq__program__inputs__rounds": 1.5}), + ["wasmq_form"], [5]), + ("wasmq rule 5: inputs an array", wasmq_job(request={"wasmq__program__inputs": []}), ["wasmq_form"], [5]), + ("wasmq rule 5: the program's digest in upper case", wasmq_job(request={ + "wasmq__program__blake3": "A1" * 32}), ["wasmq_form"], [5]), + ("wasmq rule 6: a module of no bytes", wasmq_job(request={"wasmq__modules__0__bytes": 0}), + ["wasmq_form"], [6]), + ("wasmq rule 6: the modules past 2^19 bytes together", wasmq_job(request={ + "wasmq__modules__0__bytes": 2 ** 19}), ["wasmq_form"], [6]), + ("wasmq rule 6: a prior past 2^27 bytes", wasmq_job(request={ + "wasmq__modules__0__priors__0__bytes": 2 ** 27 + 1}), ["wasmq_form"], [6]), + ("wasmq rule 6: a prior a member more", wasmq_job(request={"wasmq__modules__0__priors__0__note": "x"}), + ["wasmq_form"], [6]), + ("wasmq rule 6: priors an object", wasmq_job(request={"wasmq__modules__0__priors": {}}), + ["wasmq_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}), ["wasmq_form"], [6]), + ("wasmq rule 7: a module no callout names", wasmq_job(request={"wasmq__callouts__1__module": 0}), + ["wasmq_form"], [7]), + ("wasmq rule 7: a module index past the modules", wasmq_job(request={"wasmq__callouts__1__module": 2}), + ["wasmq_form"], [7]), + ("wasmq rule 7: an extern twice", wasmq_job(request={"wasmq__callouts__2": { + "extern": "decode64", "module": 1, "export": "decode"}}), ["wasmq_form"], [7]), + ("wasmq rule 7: a module index not whole", wasmq_job(request={"wasmq__callouts__1__module": 1.5}), + ["wasmq_form"], [7]), + ("wasmq rule 7: an export not a string", wasmq_job(request={"wasmq__callouts__0__export": 7}), + ["wasmq_form"], [7]), + ("wasmq rule 8: another state", wasmq_job(request={"wasmq__run__state": "batch"}), ["wasmq_form"], [8]), + ("wasmq rule 8: another method, alike in the result", wasmq_job( + request={"wasmq__run__method": "statevector"}, result={"method": "statevector"}), ["wasmq_form"], [8]), + ("wasmq rule 8: a seed below 0, alike in the result", wasmq_job( + request={"wasmq__run__seed": -1}, result={"seed": -1}), ["wasmq_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}), + ["wasmq_form"], [8]), + ("wasmq rule 9: a member more", wasmq_job(result={"note": "x"}), ["wasmq_form"], [9]), + ("wasmq rule 10: sealed in the first form", wasmq_job(form="first"), ["wasmq_form"], [10]), + ("wasmq rule 11: another profile", wasmq_job(result={"profile": "wai.quantum.wasmq/2"}), + ["wasmq_form"], [11]), + ("wasmq rule 11: accuracy a member more", wasmq_job(result={"accuracy__note": "x"}), ["wasmq_form"], [11]), + ("wasmq rule 11: another energy", wasmq_job(result={"energy__unmetered": "x"}), ["wasmq_form"], [11]), + ("wasmq rule 11: another seed", wasmq_job(result={"seed": 1235}), ["wasmq_form"], [11]), + ("wasmq rule 11: another method", wasmq_job(result={"method": "statevector"}), ["wasmq_form"], [11]), + ("wasmq rule 12: past 2^15 qubits", wasmq_job(result={"qubits": 2 ** 15 + 1}), ["wasmq_form"], [12]), + ("wasmq rule 12: the transcript's digest in upper case", wasmq_job(result={"transcript_blake3": "F6" * 32}), + ["wasmq_form"], [12]), + ("wasmq rule 13: fuel a member more", wasmq_job(result={"fuel__note": "x"}), ["wasmq_form"], [13]), + ("wasmq rule 13: a budget not run's", wasmq_job(result={"fuel__budget__per_call": 400}), + ["wasmq_form"], [13]), + ("wasmq rule 13: used past per_job", wasmq_job(request={"wasmq__run__fuel__per_job": 6899}, + result={"fuel__budget__per_job": 6899}), + ["wasmq_form"], [13]), + ("wasmq rule 14: a member no extern names", wasmq_job(result={"fuel__callouts__plan": _DELETE, + "fuel__callouts__plan2": plan_fuel}), + ["wasmq_form"], [14]), + ("wasmq rule 14: max past its fuel", wasmq_job(result={"fuel__callouts__plan__max": 120}), + ["wasmq_form"], [14]), + ("wasmq rule 14: fuel past calls × max", wasmq_job(result={"fuel__callouts__decode64__max": 199}), + ["wasmq_form"], [14]), + ("wasmq rule 14: max past per_call", wasmq_job(request={"wasmq__run__fuel__per_call": 150}, + result={"fuel__budget__per_call": 150}), + ["wasmq_form"], [14]), + ("wasmq rule 14: no calls", wasmq_job(result={"fuel__callouts__plan": {"calls": 0, "fuel": 0, "max": 0}, + "fuel__used": 6800, "callouts": 32}), + ["wasmq_form"], [14]), + ("wasmq rule 15: used not the sum", wasmq_job(result={"fuel__used": 6901}), ["wasmq_form"], [15]), + ("wasmq rule 15: callouts not the calls", wasmq_job(result={"callouts": 35}), ["wasmq_form"], [15]), + ("wasmq rule 16: a module named otherwise", wasmq_job(result={"modules__1__name": "other"}), + ["wasmq_form"], [16]), + ("wasmq rule 16: a module missing", wasmq_job(result={"modules__1": _DELETE}), ["wasmq_form"], [16]), + ("wasmq rule 16: a module a member more", wasmq_job(result={"modules__0__bytes": 39442}), + ["wasmq_form"], [16]), + ("wasmq rule 16: another digest", wasmq_job(result={"modules__0__blake3": "d4" * 32}), + ["wasmq_form"], [16]), + ("wasmq rule 17: instances past M × S × per_instance", wasmq_job( + request={"wasmq__run__fuel__per_instance": 2}, result={"fuel__budget__per_instance": 2}), + ["wasmq_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}), ["wasmq_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}), + ["wasmq_form"], [17]), + ("wasmq rule 18: ok, a shot not run", wasmq_job(result={"shots_run": 31, "counts__0101": 29}), + ["wasmq_form"], [18]), + ("wasmq rule 18: ok, with a fault", wasmq_job(result={"fault": { + "reason": "unreachable", "shot": 32, "phase": "seed", "module": "decoder", "fuel": 1}}), + ["wasmq_form"], [18]), + ("wasmq rule 18: ok, with a fault of null", wasmq_job(result={"fault": None}), ["wasmq_form"], [18]), + ("wasmq rule 18: another status", wasmq_job(result={"status": "done"}), ["wasmq_form"], [18]), + ("wasmq rule 18: fault, and no fault", wasmq_job(fault=True, result={"fault": _DELETE}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a reason §6.2 does not name", wasmq_job(fault=True, result={"fault__reason": "timeout"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: every shot run", wasmq_job(fault=True, result={ + "shots_run": 8, "counts__0101": 7, "fault__shot": 8}), ["wasmq_form"], [18]), + ("wasmq rule 18: the fault's shot not shots_run", wasmq_job(fault=True, result={"fault__shot": 1}), + ["wasmq_form"], [18]), + ("wasmq rule 18: another phase", wasmq_job(fault=True, result={"fault__phase": "run"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a module not the contract's", wasmq_job(fault=True, result={"fault__module": "other"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: fuel past per_call in a call", wasmq_job(fault=True, result={"fault__fuel": 401}), + ["wasmq_form"], [18]), + ("wasmq rule 18: fuel past F's used", wasmq_job(fault=True, result={ + "fault__phase": "init", "fault__budget": "instance", "fault__callout": _DELETE, "fault__fuel": 1401}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a call without its callout", wasmq_job(fault=True, result={"fault__callout": _DELETE}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a callout no extern names", wasmq_job(fault=True, result={"fault__callout": "plan2"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a callout outside a call", wasmq_job(fault=True, result={ + "fault__phase": "init", "fault__budget": "instance"}), ["wasmq_form"], [18]), + ("wasmq rule 18: fuel_exhausted without its budget", wasmq_job(fault=True, result={"fault__budget": _DELETE}), + ["wasmq_form"], [18]), + ("wasmq rule 18: a budget on a trap", wasmq_job(fault=True, result={"fault__reason": "unreachable"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: an instance's budget in a call", wasmq_job(fault=True, result={"fault__budget": "instance"}), + ["wasmq_form"], [18]), + ("wasmq rule 18: the fault a member more", wasmq_job(fault=True, result={"fault__note": "x"}), + ["wasmq_form"], [18]), + ("wasmq rule 19: outcomes of two lengths", wasmq_job(result={"counts": {"0101": 30, "110": 2}}), + ["wasmq_form"], [19]), + ("wasmq rule 19: an outcome not of 0 and 1", wasmq_job(result={"counts": {"0101": 30, "1102": 2}}), + ["wasmq_form"], [19]), + ("wasmq rule 19: a count of 0", wasmq_job(result={"counts": {"0101": 32, "1100": 0}}), + ["wasmq_form"], [19]), + ("wasmq rule 19: counts not shots_run", wasmq_job(result={"counts__0101": 29}), ["wasmq_form"], [19]), + ("wasmq rule 19: counts an array", wasmq_job(result={"counts": []}), ["wasmq_form"], [19]), + # 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]), + ) + for name, raw_case, want, rules in wasmq_cases: + check(name, reasons_of(raw_case) == want) + if rules is not None: + check(name + ": its rules", rules_of(raw_case) == rules) + alone = set(rules[0] for _, _, want, rules in wasmq_cases if want == ["wasmq_form"] and len(rules) == 1) + check("wasmq: each of rules 2 to %d broken alone" % WASMQ_RULES, alone == set(range(2, WASMQ_RULES + 1))) + wasmq_run = wasmq_job() + check("wasmq: changed after its seal, the form kept: digest_mismatch alone", reasons_of(changed_after( + wasmq_run, lambda job: job["result"].update(qubits=18))) == ["digest_mismatch"]) + check("wasmq: changed after its seal, the form broken: beside", reasons_of(changed_after( + wasmq_run, lambda job: job["result"].update(profile="x"))) == ["digest_mismatch", "wasmq_form"]) + check("wasmq: no receipt, alone", reasons_of(changed_after( + wasmq_run, lambda job: (job["result"].pop("receipt"), job["result"].update(profile="x")))) + == ["no_receipt"]) + check("wasmq: not a job, alone", reasons_of(changed_after( + wasmq_run, lambda job: job.update(request=[]))) == ["not_a_job"]) + wasmq_asked, wasmq_made = wasmq_parts(False) + check("wasmq: a set's item, valid", reasons_of(_set_body( + seed, {"programs": [wasmq_asked]}, [wasmq_asked], item_result=_dump(wasmq_made))) == []) + check("wasmq: a set's item breaking a rule", reasons_of(_set_body( + seed, {"programs": [wasmq_asked]}, [wasmq_asked], + item_result=_dump(dict(wasmq_made, profile="x")))) == ["set_item"]) + # 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,6 +2631,7 @@ 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_job(result={"profile": "wai.quantum.wasmq/2"}), ["wasmq_form"], []), ) for plant, raw, strict, lenient in plant_cases: check("plant %s: strict verdict" % plant, reasons_of(raw) == strict) diff --git a/wai/wai-quantum-cli/Cargo.lock b/wai/wai-quantum-cli/Cargo.lock index 54e625904..18285ead9 100644 --- a/wai/wai-quantum-cli/Cargo.lock +++ b/wai/wai-quantum-cli/Cargo.lock @@ -1724,7 +1724,6 @@ name = "wai-wasmq" version = "0.1.0" dependencies = [ "blake3", - "serde", "serde_json", "wai-quantum", "wasmparser", diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 3b09d527b..5947fd497 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -307,8 +307,11 @@ wai-quantum verify job.json --bundle round.wai # the signer, ho 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` names the signer -and how the run ended. `run --engine native|interpreter` chooses +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, diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 62cbea82f..9ca3fdd1f 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -1108,6 +1108,7 @@ Each construct below is supported (read as the language defines it), lowered (re | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | #### Timing @@ -1236,7 +1237,7 @@ Every code: | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000). | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -1263,6 +1264,7 @@ 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). | | `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. | @@ -1576,6 +1578,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 @@ -1872,6 +1878,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 @@ -3579,9 +3705,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 @@ -3591,6 +3716,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 bf7618243..975cf046d 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21384,10 +21384,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", @@ -21405,7 +21405,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": [ @@ -21431,6 +21431,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/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index d58babf6e..9be65c8f5 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -31362,7 +31362,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": [ @@ -31387,6 +31387,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-cli/schemas/local-tools.v1.json b/wai/wai-quantum-cli/schemas/local-tools.v1.json index 9ab0ca381..1357f76e6 100644 --- a/wai/wai-quantum-cli/schemas/local-tools.v1.json +++ b/wai/wai-quantum-cli/schemas/local-tools.v1.json @@ -7,56 +7,6 @@ "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; for a wasmQ job, also its body and its form (extensions/quantum-wasmq.md §6.5).", "anyOf": [ - { - "type": "object", - "description": "A wasmQ job, as `wai-quantum verify` judges it: valid when its seal holds and it is a job the profile writes.", - "required": [ - "valid", - "signature", - "key", - "outcome", - "problems", - "notes" - ], - "additionalProperties": false, - "properties": { - "valid": { - "type": "boolean" - }, - "signature": { - "type": "boolean", - "description": "Whether the receipt, of the form the engine seals, seals the request and the result under the key it names." - }, - "key": { - "type": [ - "string", - "null" - ], - "description": "The signing key, 32 bytes in hex." - }, - "outcome": { - "type": [ - "string", - "null" - ], - "description": "How the run ended, from the signed result: ok or fault." - }, - "problems": { - "type": "array", - "items": { - "type": "string" - }, - "description": "What makes the job other than one the profile writes, from what is sealed; empty when it is one." - }, - "notes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Where the host's unsealed words in the body (its status, its method, members nothing seals) differ from what is sealed: beside the verdict, and no part of it." - } - } - }, { "type": "object", "description": "Whether a job's receipt seals its request and its result and every rule of its form holds, and the key that signed it.", @@ -73,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": [ @@ -99,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/main.rs b/wai/wai-quantum-cli/src/main.rs index c9a3f10d1..49e980a92 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -1500,19 +1500,6 @@ fn main() { Command::Task { cmd, args } => task::main(cmd, &args), Command::Verify { file, history: hist, bundle, json: as_json, lines } => { let bytes = read_job_bytes(file.as_deref()).unwrap_or_else(|e| die(&e)); - #[cfg(feature = "wasmq")] - if let Some(text) = std::str::from_utf8(&bytes).ok() - && let Ok(v) = serde_json::from_str::(text) - && v["result"]["format"] == wai_wasmq::run::FORMAT - { - if hist.is_some() { - die("--history checks a program's run against its history; a wasmQ job carries none, so nothing could be checked"); - } - std::process::exit(if wasmq::verify(text, bundle.as_deref()) { 0 } else { 1 }); - } - if bundle.is_some() { - die("--bundle is for a wasmQ job (`run ENVELOPE.wai --json`)"); - } let src = match String::from_utf8(bytes) { Ok(s) => s, Err(e) => { @@ -1550,6 +1537,19 @@ fn main() { // 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; let set = v["result"]["format"] == wai_quantum::engine::sets::FORMAT; + // A wasmQ job is read and judged as every job is (job::check holds it to its + // profile's form); only its envelope, run again here, is its own to check. + let wasmq = wai_quantum::engine::wasmq::is_job(&v["result"]); + 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"); } @@ -1599,6 +1599,10 @@ fn main() { } said["history"] = r; } + #[cfg(feature = "wasmq")] + if let Some(b) = &bundle { + wasmq::bundle_into(&v, b, &mut said); + } println!("{said}"); if said["valid"] != true { std::process::exit(1); @@ -1607,6 +1611,10 @@ fn main() { } 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" }; diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 5d6f72f35..eb391a9ca 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -641,12 +641,6 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli if given["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { return Ok(wai_quantum::engine::variational::verify(given)); } - // A wasmQ job as `wai-quantum verify` judges it: its body, its form and its seal. - #[cfg(feature = "wasmq")] - if given["result"]["format"] == wai_wasmq::run::FORMAT { - let v = wai_wasmq::verify(given, None, wai_wasmq::run::EngineKind::default_here())?; - return Ok(json!({ "valid": v.ok(), "signature": v.signature, "key": v.signer, "outcome": v.outcome, "problems": v.well_formed, "notes": v.notes })); - } job::verdict(given) } "list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })), @@ -1234,14 +1228,20 @@ mod tests { 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}"); - assert_eq!(v["structuredContent"]["signature"], true, "{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"]["notes"][0], r#"the host's unsealed status says "done"; the sealed result says "fault""#, "{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] diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index b16c770be..b7f4aac6e 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -288,53 +288,62 @@ fn print_job(body: &Value, top: usize, kind: EngineKind) { println!("receipt digest {}, signed by {}", r["receipt"]["digest"].as_str().unwrap_or(""), r["receipt"]["key"].as_str().unwrap_or("")); } -/// `verify` for a wasmQ job, from its text: exit status 0 when it holds. -pub fn verify(text: &str, bundle: Option<&Path>) -> bool { - // Read strictly first: a member twice, or a number readers take differently, makes the - // text mean different jobs to different readers (§6.5). - let v = match wai_wasmq::run::read_job(text) { - Ok(v) => v, - Err(e) => { - println!("text NOT a job the profile writes: {e}"); - return false; - } - }; - let v = &v; - let opened = bundle.map(|p| wai_wasmq::open(&read_envelope(p)).unwrap_or_else(|r| die(&r.to_string()))); - let verdict = wai_wasmq::verify(v, opened.as_ref(), EngineKind::default_here()).unwrap_or_else(|e| die(&e)); - println!("signature {}", if verdict.signature { "VALID" } else { "INVALID" }); - println!("binds the contract (program, modules, priors, run) and the result: fuel, modules, transcript, fault"); - // Who signed, as every receipt's check names it: a receipt shows that this key sealed - // this result, and whether it is a key the reader expects is the reader's to compare. - if let Some(k) = &verdict.signer { - println!("signer {}", wai_quantum::engine::job::did_of(k).unwrap_or_else(|| k.clone())); - } - match (verdict.outcome.as_deref(), v["result"]["fault"]["reason"].as_str()) { - (Some("fault"), Some(reason)) => println!("outcome fault: {reason} in shot {}, after {} of {} shots", v["result"]["fault"]["shot"], v["result"]["shots_run"], v["result"]["shots"]), - (Some(o), _) => println!("outcome {o}: {} of {} shots", v["result"]["shots_run"], v["result"]["shots"]), +/// 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 { + let w = c.wasmq.clone().unwrap_or(Value::Null); + 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"), } - if verdict.well_formed.is_empty() { + 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 &verdict.well_formed { + 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 &verdict.notes { + for n in lines("notes") { println!("note {n}"); } - match verdict.bundle { - Some(true) => println!("bundle the envelope the receipt binds"), - Some(false) => println!("bundle NOT the envelope the receipt binds"), - None => println!("bundle not checked (--bundle FILE runs it again and compares)"), - } - match verdict.reproduced { + 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 => {} } - verdict.ok() + 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) -> ! { diff --git a/wai/wai-quantum-cli/tests/verify.rs b/wai/wai-quantum-cli/tests/verify.rs index f393ce2ea..a9ff005ae 100644 --- a/wai/wai-quantum-cli/tests/verify.rs +++ b/wai/wai-quantum-cli/tests/verify.rs @@ -179,3 +179,48 @@ 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); +} + diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index 80f87559c..4d54cee0c 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -340,7 +340,7 @@ fn a_flag_that_does_not_apply_to_an_envelope_or_a_program_is_refused_not_ignored } #[test] -fn a_job_is_read_from_its_text_strictly_before_it_is_checked() { +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()); @@ -352,6 +352,9 @@ fn a_job_is_read_from_its_text_strictly_before_it_is_checked() { 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); @@ -362,12 +365,55 @@ fn a_job_is_read_from_its_text_strictly_before_it_is_checked() { 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", j]); - assert!(!o.status.success(), "a whole number past 2^64: {}", text(&o)); - // A number with a fraction, where the profile writes whole numbers. + 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]); - assert!(!o.status.success(), "a number written with a fraction: {}", text(&o)); + 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 refused by diff --git a/wai/wai-quantum-desktop/src/main.rs b/wai/wai-quantum-desktop/src/main.rs index 1bc4c8362..0d35a2d97 100644 --- a/wai/wai-quantum-desktop/src/main.rs +++ b/wai/wai-quantum-desktop/src/main.rs @@ -1433,5 +1433,9 @@ 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"); } } 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-views/src/lib.rs b/wai/wai-quantum-views/src/lib.rs index 9c8f92b0c..224f83430 100644 --- a/wai/wai-quantum-views/src/lib.rs +++ b/wai/wai-quantum-views/src/lib.rs @@ -158,6 +158,20 @@ 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}"); + } + #[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 33004c889..03c52aeef 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/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/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. diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index 3d020880e..ddf6c291e 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -144,6 +144,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 +166,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 +178,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 +187,9 @@ impl Check { if let Some(p) = &self.provider { v["provider"] = p.clone(); } + if let Some(w) = &self.wasmq { + v["wasmq"] = w.clone(); + } v } @@ -302,6 +314,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 +335,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)), diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 0338ed620..a5917d720 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -255,4 +255,5 @@ pub mod variational; pub mod versions; #[cfg(feature = "quantum_receipt")] pub mod views; +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..7b11e7185 --- /dev/null +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -0,0 +1,495 @@ +//! 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 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 { + v.as_u64().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(v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mut Vec) { + let Some(o) = v.as_object() else { + out.push(format!("{what} is not an object")); + return; + }; + for name in required { + if !o.contains_key(*name) { + out.push(format!("{what} has no `{name}`")); + } + } + for name in o.keys() { + if !required.contains(&name.as_str()) && !optional.contains(&name.as_str()) { + out.push(format!("{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. +fn past_exact(v: &Value, path: &str) -> Option { + match v { + Value::Number(n) => n.as_f64().is_some_and(|x| x.abs() > MAX_EXACT as f64).then(|| path.to_string()), + Value::Array(a) => a.iter().enumerate().find_map(|(i, x)| past_exact(x, &format!("{path}[{i}]"))), + Value::Object(m) => m.iter().find_map(|(k, x)| past_exact(x, &format!("{path}.{k}"))), + _ => None, + } +} + +/// 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) { + let mut problem = |s: String| out.push(format!("the contract: {s}")); + let mut sub = Vec::new(); + members(c, "it", &["profile", "program", "modules", "callouts", "run"], &[], &mut sub); + members(&c["program"], "its program", &["blake3", "bytes", "inputs"], &[], &mut sub); + members(&c["run"], "its run", &["shots", "seed", "method", "state", "fuel"], &[], &mut sub); + members(&c["run"]["fuel"], "its budgets", &["per_call", "per_job", "per_instance"], &[], &mut sub); + for s in sub { + problem(s); + } + if c["profile"] != PROFILE { + problem(format!("its profile is {}, not {PROFILE}", c["profile"])); + } + let p = &c["program"]; + if !is_digest(&p["blake3"]) || !whole(&p["bytes"], 1, MAX_PROGRAM_BYTES as u64) { + problem(format!("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!("its program's inputs are {}, not whole numbers", p["inputs"])); + } + let modules = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if modules.is_empty() { + problem("it holds no module".into()); + } + let mut code = 0u64; + for m in modules { + let mut sub = Vec::new(); + members(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_CODE_BYTES as u64) { + problem(format!("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!("a module's priors are {}", m["priors"])); + } + for pr in priors { + let mut sub = Vec::new(); + members(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_ENVELOPE_BYTES as u64) { + problem(format!("a prior is {pr}, not a name, a digest and a length")); + } + } + } + if code > MAX_CODE_BYTES as u64 { + problem(format!("its modules hold {code} bytes, past {MAX_CODE_BYTES}")); + } + let callouts = c["callouts"].as_array().map(Vec::as_slice).unwrap_or(&[]); + if callouts.is_empty() { + problem("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(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!("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!("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!("module {k} is named by no callout")); + } + } + let run = &c["run"]; + if !whole(&run["shots"], 1, MAX_SHOTS) { + problem(format!("its shots are {}, where a run takes 1 to {MAX_SHOTS}", run["shots"])); + } + if !whole(&run["seed"], 0, MAX_EXACT) { + problem(format!("its seed is {}", run["seed"])); + } + if run["method"] != METHOD { + problem(format!("its method is {}, where this version of the profile has {METHOD}", run["method"])); + } + if run["state"] != "shot" && run["state"] != "job" { + problem(format!("its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); + } + let most = (MAX_HOST_WORK / FUEL_WORK) as u64; + for b in ["per_call", "per_job", "per_instance"] { + if !whole(&run["fuel"][b], 0, MAX_EXACT.min(most)) { + problem(format!("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(); + // 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!("the {what}'s `{}` is past 2^53 - 1", at.trim_start_matches('.'))); + } + } + members(req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); + if !is_digest(&req["envelope_blake3"]) { + out.push(format!("the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); + } + members(r, "the result", &RESULT, &["fault"], &mut out); + let c = &req["wasmq"]; + let run = &c["run"]; + contract_form(c, &mut out); + // 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!("the receipt's canon is {}, not rfc8785", r["receipt"]["canon"])); + } + // 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!("the result's {name} is {}, not {want}", r[name])); + } + } + if count(&r["qubits"]).is_none_or(|q| q > u64::from(MAX_QUBITS)) { + out.push(format!("the result's qubits are {}", r["qubits"])); + } + if !is_digest(&r["transcript_blake3"]) { + out.push(format!("the result's transcript_blake3 is {}, not a digest", r["transcript_blake3"])); + } + // The fuel: within its budgets, and what the calls and the instances add up to. + let fuel = &r["fuel"]; + members(fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); + if !same(&fuel["budget"], &run["fuel"]) { + out.push(format!("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!("fuel used {} is past the run's budget {per_job}", fuel["used"])); + } + 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!("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(f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); + if !externs.contains(name.as_str()) { + out.push(format!("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!("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!("callout {name}'s largest call, {}, is past its fuel, {}", f["max"], f["fuel"])); + } + if n.is_none_or(|n| n == 0) { + out.push(format!("callout {name} has fuel for {} calls", f["calls"])); + } + if all.is_none_or(|a| n.zip(max).is_none_or(|(n, m)| u128::from(a) > u128::from(n) * u128::from(m))) { + out.push(format!("callout {name}'s fuel, {}, is past its {} calls of at most {} each", f["fuel"], f["calls"], f["max"])); + } + spent = spent.zip(all).and_then(|(a, b)| a.checked_add(b)); + calls = calls.zip(n).and_then(|(a, b)| a.checked_add(b)); + } + if spent.is_none() || spent != count(&fuel["used"]) { + out.push(format!("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!("{} callouts, where the fuel counts {calls:?}", r["callouts"])); + } + // 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("the result's modules are not the ones the contract pins".into()); + } + for m in got { + members(m, "a module of the result", &["name", "blake3"], &[], &mut out); + if !is_digest(&m["blake3"]) { + out.push(format!("a module of the result whose blake3 is {}, not a digest", m["blake3"])); + } + } + // 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!("the instances' fuel, {}, is past what {made} of them could be given", fuel["instances"])); + } + // 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("a result that is ok and names a fault".into()); + } + if shots_run.is_none() || shots_run != shots { + out.push(format!("a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); + } + } + Some("fault") => { + let f = &r["fault"]; + members(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!("a fault the profile does not name: {}", f["reason"])); + } + if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) { + out.push(format!("a fault after {} of {} shots", r["shots_run"], r["shots"])); + } + if shots_run.is_none() || count(&f["shot"]) != shots_run { + out.push(format!("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!("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!("a fault in {}, a module the contract does not name", f["module"])); + } + let used = count(&f["fuel"]); + if used.is_none_or(|n| count(&fuel["used"]).is_none_or(|all| n > all)) { + out.push(format!("a fault after {} fuel, of {} used", f["fuel"], fuel["used"])); + } + let is_call = phase == Some("call"); + let budget = if is_call { per_call } else { per_instance }; + if used.is_none_or(|n| n > budget) { + out.push(format!("a fault after {} fuel, past the {budget} its 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!("a fault that names the callout {x} in phase {}", f["phase"])), + None if is_call => out.push("a call's fault that names no callout".into()), + _ => {} + } + match f.get("budget") { + Some(x) if reason != Some("fuel_exhausted") => out.push(format!("a budget, {x}, on a fault of {}", f["reason"])), + Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), + None if reason == Some("fuel_exhausted") => out.push("a fuel fault that names no budget".into()), + _ => {} + } + } + _ => out.push(format!("status {}", r["status"])), + } + // 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!("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!("an outcome that is not bits: {outcome:?}")); + } + if *width.get_or_insert(outcome.len()) != outcome.len() { + out.push(format!("outcomes of {} and {} bits", width.unwrap_or(0), outcome.len())); + } + if count(n).is_none_or(|n| n == 0) { + out.push(format!("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!("counts sum to {total:?}, not the {} shots run", r["shots_run"])); + } + listed(out) +} + +/// 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() { + 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)] { + 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()); + } + + /// 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}"); + } + } + } +} diff --git a/wai/wai-quantum/tests/wasmq_verdict.rs b/wai/wai-quantum/tests/wasmq_verdict.rs new file mode 100644 index 000000000..61b4079cc --- /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("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-wasmq/Cargo.lock b/wai/wai-wasmq/Cargo.lock index f5a4162eb..1bbed7fcf 100644 --- a/wai/wai-wasmq/Cargo.lock +++ b/wai/wai-wasmq/Cargo.lock @@ -1011,7 +1011,6 @@ name = "wai-wasmq" version = "0.1.0" dependencies = [ "blake3", - "serde", "serde_json", "wai-quantum", "wasmparser 0.261.0", diff --git a/wai/wai-wasmq/Cargo.toml b/wai/wai-wasmq/Cargo.toml index 1266cf48d..e7185b7d1 100644 --- a/wai/wai-wasmq/Cargo.toml +++ b/wai/wai-wasmq/Cargo.toml @@ -27,8 +27,6 @@ wasmtime = ["dep:wasmtime"] [dependencies] blake3 = "1" -# A job is read strictly (`run::read_job`): a visitor of serde's own refuses a member twice. -serde = "1" serde_json = { version = "1", features = ["float_roundtrip"] } wai-quantum = { version = "0.8.1", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt"] } wasmparser = { version = "0.261", default-features = false, features = ["std", "validate", "features", "simd"] } diff --git a/wai/wai-wasmq/README.md b/wai/wai-wasmq/README.md index 2c76a53c2..5e5ae9724 100644 --- a/wai/wai-wasmq/README.md +++ b/wai/wai-wasmq/README.md @@ -17,7 +17,9 @@ functions and their priors, pinned by BLAKE3. This crate: - 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 `wai-quantum verify` checks; + 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 diff --git a/wai/wai-wasmq/mutations.py b/wai/wai-wasmq/mutations.py index 3a510229b..250d419c3 100644 --- a/wai/wai-wasmq/mutations.py +++ b/wai/wai-wasmq/mutations.py @@ -23,7 +23,8 @@ 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). The tests are wasmQ's, but for a file of wai-quantum tested by -# that crate's own `--lib` tests, and the command line's for a file of the command line's. +# 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"]), @@ -92,29 +93,35 @@ M = [ ("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", WASMQ, "src/run.rs", ' "status" if value == body_status(r) => {}', ' "status" => {}', ["--test", "review"], ["a_job_whose_status_or_method"]), + ("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: key.as_str().map(str::to_string)", "signer: None", ["--test", "review"], ["a_job_names_its_signer"]), - ("the seal is checked only in its form", WASMQ, "src/run.rs", " let signature = seal_form.is_empty() && {", " let signature = {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the receipt's algorithm", WASMQ, "src/run.rs", ' if seal["alg"] != "ed25519" || seal["canon"] != "rfc8785" {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the result's members", WASMQ, "src/run.rs", ' members(r, "the result", &RESULT, &["fault"], &mut out);', "", ["--test", "sweep"], ["a_job_whose_result"]), - ("the contract's form", WASMQ, "src/run.rs", " contract_form(c, &mut out);", "", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the contract's method", WASMQ, "src/run.rs", ' if run["method"] != METHOD {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the contract's shots", WASMQ, "src/run.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", WASMQ, "src/run.rs", " if r[name] != want {", " if r[name] != want && false {", ["--test", "sweep"], ["a_job_whose_result"]), - ("fuel used within the run's budget", WASMQ, "src/run.rs", ' if fuel["used"].as_u64().is_none_or(|u| u > per_job) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("a call within the call's budget", WASMQ, "src/run.rs", ' if f["max"].as_u64().is_none_or(|m| m > per_call) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("a callout's fuel within its calls", WASMQ, "src/run.rs", ' if f["fuel"].as_u64().is_none_or(|all| f["calls"]', ' if false && f["fuel"].as_u64().is_none_or(|all| f["calls"]', ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the instances' fuel", WASMQ, "src/run.rs", ' if fuel["instances"].as_u64().is_none_or(|n| u128::from(n) > made * stretches * u128::from(per_instance)) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("a fault's fuel within its phase's budget", WASMQ, "src/run.rs", ' if f["fuel"].as_u64().is_none_or(|n| n > budget) {', " if false {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), - ("the fuel adds up", WASMQ, "src/run.rs", ' if spent != fuel["used"].as_u64() {', ' if spent != fuel["used"].as_u64() && false {', ["--test", "sweep"], ["a_job_whose_result"]), - ("a fault after every shot", WASMQ, "src/run.rs", " if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) {", " if false {", ["--test", "sweep"], ["a_job_whose_result"]), - ("the counts' sum", WASMQ, "src/run.rs", " if total != shots_run {", " if total != shots_run && false {", ["--test", "sweep"], ["a_job_whose_result"]), + ("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 {", ["--test", "sweep"], ["a_job_whose_contract_or_receipt"]), + ("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 strictly", CLI, "src/wasmq.rs", " let v = match wai_wasmq::run::read_job(text) {", " let v = match serde_json::from_str::(text).map_err(|e| e.to_string()) {", ["--test", "wasmq"], ["a_job_is_read_from_its_text_strictly"]), - ("no history for a wasmQ job", CLI, "src/main.rs", " if hist.is_some() {\n die(", " if false {\n die(", ["--test", "wasmq"], ["a_flag_that_does_not_apply"]), + ("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"]), + # 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"]), ] @@ -125,7 +132,8 @@ def run(name, crate, path, old, new, args, tests): 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. - tested = crate if "--lib" in args or crate == CLI else WASMQ + # A crate's own tests run there too when the entry asks for its features. + tested = 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 diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index 9ddf64508..f45eeb5b7 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -15,25 +15,14 @@ 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::{FUEL_WORK, MAX_CODE_BYTES, 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 shots a run takes. -pub const MAX_SHOTS: u64 = 1 << 20; -/// The largest number a budget, a seed, an input or a count may be: every JSON reader -/// holds it exactly. -pub const MAX_EXACT: u64 = (1 << 53) - 1; -/// The most qubits a program may declare: what the method's tableau holds. -pub const MAX_QUBITS: u32 = 1 << 15; -/// The one method of this version of the profile (§2.2). -pub const METHOD: &str = "stabilizer"; -/// The most bytes an envelope holds (§2.3): a reader holds them and its frames at once, -/// and what one host opens every host must be able to. -pub const MAX_ENVELOPE_BYTES: usize = 1 << 27; -/// The most bytes a program's text may have (§4.8): a reader holds its tokens, some eighty -/// bytes for each byte of text, before it reads a statement. -pub const MAX_PROGRAM_BYTES: usize = 1 << 20; /// 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 a program holds, its blocks' own among them, and any one value or array (§4.8). @@ -42,13 +31,8 @@ pub const MAX_BITS: u32 = 1 << 20; 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 most bytes of module the modules of a bundle hold among them (§3.2, §4.8). -pub const MAX_CODE_BYTES: usize = 1 << 19; /// The most bits a run's results may hold: its shots times the bits of a shot (§4.8). pub const MAX_RESULT_BITS: u128 = 1 << 28; -/// The most work a run may ask of its host (§4.8): the host's own part, and the most fuel -/// it may be given. -pub const MAX_HOST_WORK: u128 = 1 << 42; /// The host's part of a call into a module, or of making an instance, as [`host_work`] /// counts it. pub const CALL_WORK: u128 = 1 << 12; @@ -58,9 +42,6 @@ pub const BIT_WORK: u128 = 4; /// 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; -/// A unit of fuel, in the host's work: what the slowest engine, the interpreter under -/// wasm32, takes to run it (§4.2). -pub const FUEL_WORK: u128 = 8; /// 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). diff --git a/wai/wai-wasmq/src/profile.rs b/wai/wai-wasmq/src/profile.rs index beefd25ab..41029d749 100644 --- a/wai/wai-wasmq/src/profile.rs +++ b/wai/wai-wasmq/src/profile.rs @@ -16,8 +16,9 @@ use wasmparser::{ WasmFeatures, }; -/// The profile's name, as a contract and a receipt write it. -pub const PROFILE: &str = "wai.quantum.wasmq/1"; +/// 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. diff --git a/wai/wai-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs index 853237f71..8a8a22a18 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -17,29 +17,10 @@ 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. -pub const FORMAT: &str = "wai.quantum.wasmq.result/1"; -/// The members of a result (§6.4), `fault` aside, which a run that stopped has. -pub const RESULT: [&str; 16] = ["format", "profile", "status", "method", "qubits", "shots", "shots_run", "seed", "counts", "callouts", "fuel", "modules", "transcript_blake3", "accuracy", "energy", "receipt"]; -/// What a result's `energy` says (§6.4): no meter reads a run. -pub const UNMETERED: &str = "the runner reads no energy meter; fuel is the profile's measure of work"; -/// The members of a job body (§6.4). Only `request` and `result` are under the signature; -/// `status` and `method` repeat what the result says, and [`verify`] notes where they do not. -pub const BODY: [&str; 4] = ["status", "method", "request", "result"]; -/// The fault reasons a receipt may name (§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 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)] @@ -566,12 +547,6 @@ pub fn request(opened: &Opened) -> Value { json!({ "wasmq": opened.contract, "envelope_blake3": opened.envelope_blake3 }) } -/// The status a job body carries for a result (§6.4): `done`, or `fault` for a run that -/// stopped. -pub fn body_status(result: &Value) -> &'static str { - if result["status"] == "ok" { "done" } else { "fault" } -} - /// 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 { @@ -583,408 +558,6 @@ pub fn seal(opened: &Opened, mut result: Value, key: &SigningKey) -> Value { json!({ "status": status, "method": result["method"], "request": req, "result": result }) } -/// Holds `v` to exactly these members: a message for each it lacks, or holds besides. -fn members(v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mut Vec) { - let Some(o) = v.as_object() else { - out.push(format!("{what} is not an object")); - return; - }; - for name in required { - if !o.contains_key(*name) { - out.push(format!("{what} has no `{name}`")); - } - } - for name in o.keys() { - if !required.contains(&name.as_str()) && !optional.contains(&name.as_str()) { - out.push(format!("{what} holds `{name}`, which the text does not define")); - } - } -} - -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'))) -} - -/// Whether `v` is a whole number from `lo` to `hi`. -fn whole(v: &Value, lo: u64, hi: u64) -> bool { - v.as_u64().is_some_and(|n| (lo..=hi).contains(&n)) -} - -/// 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) { - use crate::envelope::{MAX_CODE_BYTES, MAX_ENVELOPE_BYTES, MAX_EXACT, MAX_SHOTS, METHOD}; - let mut problem = |s: String| out.push(format!("the contract: {s}")); - let mut sub = Vec::new(); - members(c, "it", &["profile", "program", "modules", "callouts", "run"], &[], &mut sub); - members(&c["program"], "its program", &["blake3", "bytes", "inputs"], &[], &mut sub); - members(&c["run"], "its run", &["shots", "seed", "method", "state", "fuel"], &[], &mut sub); - members(&c["run"]["fuel"], "its budgets", &["per_call", "per_job", "per_instance"], &[], &mut sub); - for s in sub { - problem(s); - } - if c["profile"] != profile::PROFILE { - problem(format!("its profile is {}, not {}", c["profile"], profile::PROFILE)); - } - let p = &c["program"]; - if !is_digest(&p["blake3"]) || !whole(&p["bytes"], 1, crate::envelope::MAX_PROGRAM_BYTES as u64) { - problem(format!("its program is {}, not a digest and a length", p)); - } - if !p["inputs"].as_object().is_some_and(|m| m.values().all(|v| v.as_i64().is_some_and(|i| i.unsigned_abs() <= MAX_EXACT))) { - problem(format!("its program's inputs are {}, not whole numbers", p["inputs"])); - } - let modules = c["modules"].as_array().map(Vec::as_slice).unwrap_or(&[]); - if modules.is_empty() { - problem("it holds no module".into()); - } - let mut code = 0u64; - for m in modules { - let mut sub = Vec::new(); - members(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_CODE_BYTES as u64) { - problem(format!("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(m["bytes"].as_u64().unwrap_or(0)); - let priors = m["priors"].as_array().map(Vec::as_slice).unwrap_or(&[]); - if !m["priors"].is_array() { - problem(format!("a module's priors are {}", m["priors"])); - } - for pr in priors { - let mut sub = Vec::new(); - members(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_ENVELOPE_BYTES as u64) { - problem(format!("a prior is {pr}, not a name, a digest and a length")); - } - } - } - if code > MAX_CODE_BYTES as u64 { - problem(format!("its modules hold {code} bytes, past {MAX_CODE_BYTES}")); - } - let callouts = c["callouts"].as_array().map(Vec::as_slice).unwrap_or(&[]); - if callouts.is_empty() { - problem("it names no callout".into()); - } - let mut externs = std::collections::BTreeSet::new(); - for x in callouts { - let mut sub = Vec::new(); - members(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!("a callout is {x}, not an extern, a module it holds and an export")); - } - if !externs.insert(x["extern"].to_string()) { - problem(format!("two callouts for the extern {}", x["extern"])); - } - } - for k in 0..modules.len() { - if !callouts.iter().any(|x| x["module"].as_u64() == Some(k as u64)) { - problem(format!("module {k} is named by no callout")); - } - } - let run = &c["run"]; - if !whole(&run["shots"], 1, MAX_SHOTS) { - problem(format!("its shots are {}, where a run takes 1 to {MAX_SHOTS}", run["shots"])); - } - if !whole(&run["seed"], 0, MAX_EXACT) { - problem(format!("its seed is {}", run["seed"])); - } - if run["method"] != METHOD { - problem(format!("its method is {}, where this version of the profile has {METHOD}", run["method"])); - } - if run["state"] != "shot" && run["state"] != "job" { - problem(format!("its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); - } - let most = (crate::envelope::MAX_HOST_WORK / crate::envelope::FUEL_WORK) as u64; - for b in ["per_call", "per_job", "per_instance"] { - if !whole(&run["fuel"][b], 0, MAX_EXACT.min(most)) { - problem(format!("its fuel {b} is {}, past what a run may be given", run["fuel"][b])); - } - } -} - -/// Holds a receipt to the form the engine seals (§6.4): Ed25519 over the RFC 8785 form, a -/// key, a digest and a signature in lowercase hexadecimal. What else a receipt holds is -/// not the seal's, and a verdict takes nothing from it. -fn receipt_form(seal: &Value, out: &mut Vec) { - if !seal.is_object() { - out.push(format!("the receipt is {seal}, not an object")); - } - if seal["alg"] != "ed25519" || seal["canon"] != "rfc8785" { - out.push(format!("the receipt is of {} over {}, not ed25519 over rfc8785", seal["alg"], seal["canon"])); - } - for (name, len) in [("key", 64), ("digest", 64), ("signature", 128)] { - if !lower_hex(&seal[name], len) { - out.push(format!("the receipt's {name} is not {len} lowercase hexadecimal digits")); - } - } -} - -fn lower_hex(v: &Value, len: usize) -> bool { - v.as_str().is_some_and(|s| s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))) -} - -/// Reads a job from its text strictly, before anything is checked (§6.5): no member twice, -/// since readers differ on which they keep, and every number a whole number a double holds -/// exactly, written as one, since readers differ on any other. -pub fn read_job(text: &str) -> Result { - use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor}; - struct Strict(Value); - struct V; - impl<'de> Visitor<'de> for V { - type Value = Value; - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("a job of the profile") - } - fn visit_bool(self, v: bool) -> Result { - Ok(Value::Bool(v)) - } - fn visit_i64(self, v: i64) -> Result { - if v.unsigned_abs() > crate::envelope::MAX_EXACT { - return Err(E::custom(format!("{v} is a whole number past 2^53 - 1"))); - } - Ok(Value::from(v)) - } - fn visit_u64(self, v: u64) -> Result { - if v > crate::envelope::MAX_EXACT { - return Err(E::custom(format!("{v} is a whole number past 2^53 - 1"))); - } - Ok(Value::from(v)) - } - fn visit_f64(self, v: f64) -> Result { - Err(E::custom(format!("{v}: the profile writes only whole numbers, without a fraction or an exponent"))) - } - fn visit_str(self, v: &str) -> Result { - Ok(Value::String(v.to_string())) - } - fn visit_unit(self) -> Result { - Ok(Value::Null) - } - fn visit_seq>(self, mut seq: A) -> Result { - let mut v = Vec::new(); - while let Some(Strict(x)) = seq.next_element()? { - v.push(x); - } - Ok(Value::Array(v)) - } - fn visit_map>(self, mut map: A) -> Result { - let mut m = Map::new(); - while let Some(k) = map.next_key::()? { - let Strict(v) = map.next_value()?; - if m.insert(k.clone(), v).is_some() { - return Err(de::Error::custom(format!("the member {k:?} twice"))); - } - } - Ok(Value::Object(m)) - } - } - impl<'de> Deserialize<'de> for Strict { - fn deserialize>(d: D) -> Result { - d.deserialize_any(V).map(Strict) - } - } - serde_json::from_str::(text).map(|s| s.0).map_err(|e| e.to_string()) -} - -/// 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. -fn unsealed(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")); - } - } - out -} - -/// What makes a job other than one this profile writes (§6.4, §6.5). A signature shows -/// that a key sealed a job, and says nothing of what the job holds: so without the -/// envelope to run again, this is what holds a result to its contract and to itself. -fn form(job: &Value) -> Vec { - let mut out = Vec::new(); - let (req, r) = (&job["request"], &job["result"]); - // A number a double cannot hold would be read differently by readers of the receipt. - for (what, value) in [("request", req), ("result", r)] { - if let Some(at) = wai_quantum::engine::receipt::inexact_number(value) { - out.push(format!("the {what}'s `{at}` is a whole number past 2^53 - 1")); - } - } - members(req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); - if !is_digest(&req["envelope_blake3"]) { - out.push(format!("the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); - } - members(r, "the result", &RESULT, &["fault"], &mut out); - let c = &req["wasmq"]; - let run = &c["run"]; - contract_form(c, &mut out); - receipt_form(&r["receipt"], &mut out); - // What the result repeats from the contract, or fixes. - let fixed = [("format", json!(FORMAT)), ("profile", json!(profile::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 r[name] != want { - out.push(format!("the result's {name} is {}, not {want}", r[name])); - } - } - if r["qubits"].as_u64().is_none_or(|q| q > u64::from(crate::envelope::MAX_QUBITS)) { - out.push(format!("the result's qubits are {}", r["qubits"])); - } - if !is_digest(&r["transcript_blake3"]) { - out.push(format!("the result's transcript_blake3 is {}, not a digest", r["transcript_blake3"])); - } - // The fuel: within its budgets, and what the calls and the instances add up to. - let fuel = &r["fuel"]; - members(fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); - if fuel["budget"] != run["fuel"] { - out.push(format!("the fuel's budget is {}, where the contract gives {}", fuel["budget"], run["fuel"])); - } - let per_job = run["fuel"]["per_job"].as_u64().unwrap_or(0); - if fuel["used"].as_u64().is_none_or(|u| u > per_job) { - out.push(format!("fuel used {} is past the run's budget {per_job}", fuel["used"])); - } - let per_call = run["fuel"]["per_call"].as_u64().unwrap_or(0); - let externs: Vec<&Value> = c["callouts"].as_array().into_iter().flatten().map(|x| &x["extern"]).collect(); - if !fuel["callouts"].is_object() { - out.push(format!("the fuel of the callouts is {}, not an object", fuel["callouts"])); - } - let mut spent = fuel["instances"].as_u64(); - let mut calls = Some(0u64); - for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { - members(f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); - if !externs.iter().any(|e| e.as_str() == Some(name)) { - out.push(format!("fuel for {name}, a callout the contract does not name")); - } - if f["max"].as_u64().is_none_or(|m| m > per_call) { - out.push(format!("callout {name} used {} in one call, past the budget {per_call}", f["max"])); - } - if f["max"].as_u64().is_none_or(|m| f["fuel"].as_u64().is_none_or(|all| m > all)) { - out.push(format!("callout {name}'s largest call, {}, is past its fuel, {}", f["max"], f["fuel"])); - } - if f["calls"].as_u64().is_none_or(|n| n == 0) { - out.push(format!("callout {name} has fuel for {} calls", f["calls"])); - } - if f["fuel"].as_u64().is_none_or(|all| f["calls"].as_u64().zip(f["max"].as_u64()).is_none_or(|(n, m)| u128::from(all) > u128::from(n) * u128::from(m))) { - out.push(format!("callout {name}'s fuel, {}, is past its {} calls of at most {} each", f["fuel"], f["calls"], f["max"])); - } - spent = spent.zip(f["fuel"].as_u64()).and_then(|(a, b)| a.checked_add(b)); - calls = calls.zip(f["calls"].as_u64()).and_then(|(a, b)| a.checked_add(b)); - } - if spent != fuel["used"].as_u64() { - out.push(format!("fuel used {}, where the instances and the calls add up to {spent:?}", fuel["used"])); - } - if calls != r["callouts"].as_u64() { - out.push(format!("{} callouts, where the fuel counts {calls:?}", r["callouts"])); - } - // The modules the contract pins. - let mods = c["modules"].as_array().cloned().unwrap_or_default(); - let got = r["modules"].as_array().cloned().unwrap_or_default(); - if mods.len() != got.len() || mods.iter().zip(&got).any(|(a, b)| a["blake3"] != b["blake3"] || a["name"] != b["name"]) { - out.push("the result's modules are not the ones the contract pins".into()); - } - for m in &got { - members(m, "a module of the result", &["name", "blake3"], &[], &mut out); - if !is_digest(&m["blake3"]) { - out.push(format!("a module of the result whose blake3 is {}, not a digest", m["blake3"])); - } - } - // 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 = run["fuel"]["per_instance"].as_u64().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(r["shots_run"].as_u64().unwrap_or(0)) + u128::from(r["status"] == "fault") }; - if fuel["instances"].as_u64().is_none_or(|n| u128::from(n) > made * stretches * u128::from(per_instance)) { - out.push(format!("the instances' fuel, {}, is past what {made} of them could be given", fuel["instances"])); - } - // How it ended. - let (shots, shots_run) = (r["shots"].as_u64(), r["shots_run"].as_u64()); - match r["status"].as_str() { - Some("ok") => { - if r.get("fault").is_some() { - out.push("a result that is ok and names a fault".into()); - } - if shots_run != shots { - out.push(format!("a result that is ok after {} of {} shots", r["shots_run"], r["shots"])); - } - } - Some("fault") => { - let f = &r["fault"]; - members(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!("a fault the profile does not name: {}", f["reason"])); - } - if shots_run.zip(shots).is_none_or(|(ran, all)| ran >= all) { - out.push(format!("a fault after {} of {} shots", r["shots_run"], r["shots"])); - } - if f["shot"].as_u64() != shots_run { - out.push(format!("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!("a fault in a phase the profile does not name: {}", f["phase"])); - } - if !mods.iter().any(|m| m["name"] == f["module"]) { - out.push(format!("a fault in {}, a module the contract does not name", f["module"])); - } - if f["fuel"].as_u64().is_none_or(|n| fuel["used"].as_u64().is_none_or(|all| n > all)) { - out.push(format!("a fault after {} fuel, of {} used", f["fuel"], fuel["used"])); - } - let is_call = phase == Some("call"); - let budget = if is_call { per_call } else { per_instance }; - if f["fuel"].as_u64().is_none_or(|n| n > budget) { - out.push(format!("a fault after {} fuel, past the {budget} its phase is given", f["fuel"])); - } - match f.get("callout") { - Some(x) if !is_call || !externs.contains(&x) => out.push(format!("a fault that names the callout {x} in phase {}", f["phase"])), - None if is_call => out.push("a call's fault that names no callout".into()), - _ => {} - } - match f.get("budget") { - Some(x) if reason != Some("fuel_exhausted") => out.push(format!("a budget, {x}, on a fault of {}", f["reason"])), - Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), - None if reason == Some("fuel_exhausted") => out.push("a fuel fault that names no budget".into()), - _ => {} - } - } - _ => out.push(format!("status {}", r["status"])), - } - // 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!("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!("an outcome that is not bits: {outcome:?}")); - } - if *width.get_or_insert(outcome.len()) != outcome.len() { - out.push(format!("outcomes of {} and {} bits", width.unwrap_or(0), outcome.len())); - } - if n.as_u64().is_none_or(|n| n == 0) { - out.push(format!("a count of {n} for {outcome:?}")); - } - total = total.zip(n.as_u64()).and_then(|(a, b)| a.checked_add(b)); - } - if total != shots_run { - out.push(format!("counts sum to {total:?}, not the {} shots run", r["shots_run"])); - } - out -} - /// What [`verify`] found. #[derive(Debug, Default)] pub struct Verdict { @@ -1016,32 +589,43 @@ impl Verdict { } } -/// Checks a wasmQ job body by what is sealed: its signature and who signed; that its -/// request and result are ones this profile writes (fuel within budgets, a fault reason it -/// names); 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. +/// 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 { - let (Some(request), Some(r)) = (job.get("request"), job.get("result")) else { + 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")?; - // The seal alone, as the engine checks a receipt: the key it names sealed this request - // and this result. Only a receipt of the form the engine seals is checked at all. - let mut seal_form = Vec::new(); - receipt_form(seal, &mut seal_form); - let signature = seal_form.is_empty() && { - let mut bare = r.clone(); - if let Some(o) = bare.as_object_mut() { - o.remove("receipt"); - } - wai_quantum::engine::receipt::verify(request, &bare, seal) + 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() }; - let key = seal["key"].clone(); - let c = &job["request"]["wasmq"]; - let mut v = Verdict { signature, signer: key.as_str().map(str::to_string), outcome: r["status"].as_str().map(str::to_string), well_formed: form(job), notes: unsealed(job), ..Verdict::default() }; if let Some(o) = bundle { - let same = o.envelope_blake3 == job["request"]["envelope_blake3"] && &o.contract == c; + // 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]); diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs index 92d3f5ffe..3cf551d0b 100644 --- a/wai/wai-wasmq/tests/review.rs +++ b/wai/wai-wasmq/tests/review.rs @@ -180,6 +180,21 @@ fn a_job(module: &str, per_call: u64) -> (wai_wasmq::Opened, Value) { 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 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 diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index 1bfc96fe5..b3990260d 100644 --- a/wai/wai-web/src/quantum_engine.rs +++ b/wai/wai-web/src/quantum_engine.rs @@ -793,6 +793,10 @@ 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"); } /// A check every build answers with the same bytes: the noisy lesson's -- 2.54.0 (Apple Git-157) From ebc13094839ff1a1f62fc272348acba2b3ac3c54 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 08:15:18 -0500 Subject: [PATCH 09/17] wai-quantum-cli: the agent docs regenerated after the merge Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum-cli/docs/llms-full.txt | 137 ++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 03d6b8fb2..44a59254d 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -1108,6 +1108,7 @@ Each construct below is supported (read as the language defines it), lowered (re | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | #### Timing @@ -1236,7 +1237,7 @@ Every code: | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16 written in standard gates, 255 on a gate a method holds whole). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000), or that holds gates whole on more qubits than that in all. | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -1263,6 +1264,7 @@ 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). | | `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. | @@ -1576,6 +1578,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 @@ -1872,6 +1878,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 @@ -3579,9 +3705,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 @@ -3591,6 +3716,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 -- 2.54.0 (Apple Git-157) From 4816bc0f911656cd399438085741eca4fe58dfb5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 11:38:43 -0500 Subject: [PATCH 10/17] wasmQ round 3, in progress: the form, the surfaces, the second verifier and the differential Co-Authored-By: Claude Opus 5.5 --- wai/extensions/quantum-providers.md | 6 +- wai/extensions/quantum-wasmq.md | 53 ++- .../tools/receipt_differential.py | 184 +++++++++++ .../tools/receipt_verify.py | 311 +++++++++++++++++- wai/wai-quantum-cli/src/main.rs | 51 ++- wai/wai-quantum-cli/src/mcp.rs | 9 +- wai/wai-quantum-cli/src/wasmq.rs | 18 +- wai/wai-quantum-desktop/src/main.rs | 5 + wai/wai-quantum-py/python/wai_quantum/_api.py | 13 +- wai/wai-quantum-py/tests/test_engine.py | 17 + wai/wai-quantum-service/README.md | 2 +- wai/wai-quantum-service/src/http.rs | 21 +- wai/wai-quantum-service/src/mcp.rs | 19 +- wai/wai-quantum-service/tests/verify.rs | 43 +++ wai/wai-quantum-views/src/lib.rs | 12 +- wai/wai-quantum/src/engine/job.rs | 39 ++- wai/wai-quantum/src/engine/wasmq.rs | 109 +++++- wai/wai-quantum/src/qasm/elab.rs | 2 +- wai/wai-wasmq/src/envelope.rs | 9 +- wai/wai-wasmq/tests/review.rs | 35 ++ wai/wai-wasmq/tests/vectors.rs | 5 + wai/wai-web/src/quantum_engine.rs | 9 +- 22 files changed, 855 insertions(+), 117 deletions(-) diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index 964879c9a..bfd0097f3 100644 --- a/wai/extensions/quantum-providers.md +++ b/wai/extensions/quantum-providers.md @@ -742,9 +742,11 @@ 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`). 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 index ceea276b0..f29d72e88 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -76,7 +76,7 @@ other member: `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 qubits the program declares, the shots and the seed. +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). @@ -102,12 +102,16 @@ frames = program | for each module, in order: the module, then each of its prio `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`, each -budget and the size of each input are at most 2^53 − 1, so that every JSON reader holds -them exactly, and the budgets are within what a run may be given (§4.8). Every module is -named by at least one callout. The program's text is at most 1 MiB, and the modules' +(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, the most fuel a run may be given (§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: @@ -540,8 +544,9 @@ takes the stream's next value `x`, and is 1 when `x < 2^63`. A `reset` measures 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 bits, bit `n − 1` first, and the counts sum the shots -that read each outcome. A program that measures nothing has as its outcome the bits its +A shot's outcome is its program's bits, `n` of them, 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 program that measures nothing has as its outcome the bits its calls write: no qubit is read for it. ### 6.2 Faults @@ -587,11 +592,11 @@ The result has exactly these members: | `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 qubits the program declares, the contract's shots and seed | +| `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` its calls, their fuel and the most one of them used | +| `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 | @@ -607,10 +612,13 @@ the receipt's digest is the same everywhere, and under one key the whole job is. 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, the -Python package, the desktop app, a view and a page. The job is read from its text as §8.2 -reads every job's, so its seal and its numbers are held as every receipt's are. A result +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 @@ -624,7 +632,9 @@ whose value is a whole number from 0 to 2^53 − 1, however it is written (`1`, 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 list or a map named below that is not an +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 @@ -687,8 +697,10 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 of at most `F`'s `used` and of at most its phase's budget (the budget - `per_call` in a call, `per_instance` otherwise, 0 when that is not a count); it has a `callout` exactly - when its phase is `call`, and that is the `extern` of one of `C`'s callouts; and it has + `per_call` in a call, `per_instance` otherwise, 0 when that is not a count); 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 @@ -703,8 +715,12 @@ sealed result says `fault`"). A note is no part of the verdict: the verdict is t 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, and `--lines` one verdict a line, each a job's -whatever its kind. `verify` reads at most 1 GiB, more than any job holds, and refuses a +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, or with no receipt is reported alone (§8.4): `verify` says that its +form was not read, and runs no envelope for it. `verify` reads at most 1 GiB, more than any job holds, and refuses a file past it by its size, before it reads it. 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 @@ -778,6 +794,7 @@ feasible fills the target, and atoms are moved, never made or lost. 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; and a program whose resets and measurements pin §6.1's stream; - `refuse/`: modules and envelopes a runner MUST refuse, each with its code diff --git a/wai/quantum-conformance/tools/receipt_differential.py b/wai/quantum-conformance/tools/receipt_differential.py index cb46ebd0e..dbf38c6a8 100644 --- a/wai/quantum-conformance/tools/receipt_differential.py +++ b/wai/quantum-conformance/tools/receipt_differential.py @@ -217,6 +217,166 @@ 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 = (load("cli-wasmq-%s.json" % n) for n in ("run", "fault", "fault-init", "state-job")) + out = [] + + def case(rule, what, base, edit, valid): + b = copy.deepcopy(base) + edit(b) + out.append(("rule %s, %s" % (rule, what), seal(b), [] 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 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} + 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, "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 2^27 bytes", run, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27), True) + case(6, "a prior of 2^27 + 1 bytes", run, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 + 1), 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) + # 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"): + case(8, "the budget %s at 2^39" % name, run, budget(name, 2 ** 39), True) + case(8, "the budget %s at 2^39 + 1" % name, run, budget(name, 2 ** 39 + 1), False) + 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, "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", run, 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(14, "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(18, "a call's fuel fault of the run's budget", fault, at(R + ("fault", "budget"), "run"), True) + 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={}, used=0), b["result"]["fault"].update(fuel=0)), 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=400, max=400)), 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=400, max=200)), 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) + return out + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--reference", help="the wai-quantum binary") @@ -253,6 +413,8 @@ def main(): 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())) return finish(a, problems) # Receipts a person holds: both must find them valid. @@ -311,9 +473,31 @@ def main(): 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()))) + problems += rules_stage(ref, a.plant, seal) + 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(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): + 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", [])))) + rules = len({w.split(",")[0] for w, _, _ in cases}) + print("rules: %d cases at the bounds of %d rules of a wasmQ job's form, sealed again; %d as written" % (len(cases), rules, 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 cf01b8226..869f3fd8b 100644 --- a/wai/quantum-conformance/tools/receipt_verify.py +++ b/wai/quantum-conformance/tools/receipt_verify.py @@ -17,7 +17,16 @@ 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: @@ -41,12 +50,14 @@ 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 - wasmq-unchecked skip the wasmQ rules (quantum-wasmq §6.5): no wasmq_form + 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. Where -quantum-wasmq §6.5 leaves one, the reading taken is marked SPEC-GAP [W1] to [W8]. +quantum-wasmq §6.5 leaves one, the reading taken is marked SPEC-GAP [W1] to [W14]. """ import hashlib @@ -1044,6 +1055,13 @@ def provider_statement(result): # 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. +# +# §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" @@ -1060,7 +1078,8 @@ WASMQ_FAULT_REASONS = ( "integer_overflow", "invalid_conversion_to_integer", "bad_result") WASMQ_PHASES = ("seed", "prior", "init", "call") WASMQ_RULES = 19 -_ABSENT = object() # a member that is not there: equal to nothing +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): @@ -1103,6 +1122,7 @@ def _arr(v): 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 @@ -1282,7 +1302,7 @@ def wasmq_broken(request, result): 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, used, modules, externs, - per_call, per_instance)) + f_callouts, per_call, per_instance)) else: rule(18, False) # any other status breaks the form @@ -1299,8 +1319,10 @@ def wasmq_broken(request, result): return sorted(broken) -def _fault_holds(fault, shots, shots_run, used, modules, externs, per_call, per_instance): - """§6.5 rule 18, for a result whose status is fault.""" +def _fault_holds(fault, shots, shots_run, used, modules, externs, f_callouts, per_call, per_instance): + """§6.5 rule 18, for a result whose status is fault. `f_callouts` is F's callouts, + read as an empty object when it is not one (rule 14 then fails, and so does this + clause: no member is named in it).""" required = {"reason", "shot", "phase", "module", "fuel"} if not (isinstance(fault, dict) and required <= set(fault) <= required | {"callout", "budget"}): return False @@ -1315,21 +1337,146 @@ def _fault_holds(fault, shots, shots_run, used, modules, externs, per_call, per_ and any(_same(fault["module"], _get(m, "name")) for m in modules) and _count(fault["fuel"]) and _count(used) and _n(fault["fuel"]) <= _n(used) and _n(fault["fuel"]) <= _or_zero(phase_budget) - # a callout exactly in a call, one of C's externs + # a callout exactly in a call (present, whatever its value), one of C's + # externs 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 and ("callout" in fault) == in_call - and (not in_call or any(_same(fault["callout"], e) for e in externs)) + and (not in_call or (any(_same(fault["callout"], e) for e in externs) + and _stopped_call_made(fault["callout"], fault["fuel"], f_callouts))) # a budget exactly for fuel_exhausted: run, or call in a call and instance otherwise and ("budget" in fault) == _text_is(reason, "fuel_exhausted") and ("budget" not in fault or _text_is(fault["budget"], "run", "call" if in_call else "instance"))) +def _stopped_call_made(callout, fuel, f_callouts): + """§6.5 rule 18: the fault's callout names a member of F's callouts whose max is at + least the fault's fuel (a count, checked before this is asked).""" + # SPEC-GAP: [W9] "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 second "that" read as + # the extern (a callout names a module and an export, not a member of F), so the + # fault's callout is one of C's externs, and F's callouts has a member of that very + # name whose max is a count at least the fault's fuel. "At least" compares counts + # as "at most" does: a max that is no count fails it. The fault's module is not + # held to the module of that callout: the rule says only that it is the name of one + # of C's modules, and names are for people (§2.2), so none is read here. + entry = _get(f_callouts, callout) if isinstance(callout, str) else _ABSENT + top = _get(entry, "max") + return _count(top) and _n(top) >= _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. + """§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 - if is_wasmq(result) and wasmq_broken(request, result): + 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 of at most 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", +} + + +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 19, 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 @@ -1347,16 +1494,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: @@ -1370,10 +1521,17 @@ def judge_job(request, result): 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. - check_wasmq(request, result, reasons) + 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 @@ -2410,6 +2568,10 @@ def self_test(args): 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={ + "fault__fuel": 150, "fault__budget": "run"}), [], []), + ("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. @@ -2557,6 +2719,19 @@ def self_test(args): ["wasmq_form"], [18]), ("wasmq rule 18: the fault a member more", wasmq_job(fault=True, result={"fault__note": "x"}), ["wasmq_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"}), ["wasmq_form"], [18]), + ("wasmq rule 18: a call's fuel past its callout's max", wasmq_job(fault=True, result={ + "fuel__callouts__decode64__max": 399}), ["wasmq_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}), ["wasmq_form"], [18]), + ("wasmq rule 18: a callout of null outside a call, present", wasmq_job(fault=True, result={ + "fault__phase": "init", "fault__budget": "instance", "fault__callout": None}), ["wasmq_form"], [18]), + ("wasmq rule 18: a budget of null on a trap, present", wasmq_job(fault=True, result={ + "fault__reason": "unreachable", "fault__budget": None}), ["wasmq_form"], [18]), ("wasmq rule 19: outcomes of two lengths", wasmq_job(result={"counts": {"0101": 30, "110": 2}}), ["wasmq_form"], [19]), ("wasmq rule 19: an outcome not of 0 and 1", wasmq_job(result={"counts": {"0101": 30, "1102": 2}}), @@ -2600,6 +2775,89 @@ def self_test(args): seed, {"programs": [wasmq_asked]}, [wasmq_asked], item_result=_dump(dict(wasmq_made, profile="x")))) == ["set_item"]) + # 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. + def statement(outcome, problems=(), notes=()): + return {"outcome": outcome, "problems": list(problems), "notes": list(notes)} + + sealed_line = '; the sealed result says ' + statement_cases = ( + ("a run", wasmq_job(), [], statement("ok")), + ("a run stopped by a fault", wasmq_job(fault=True, envelope='"status":"fault","method":"stabilizer",'), + [], statement("fault")), + ("a job breaking one rule", wasmq_job(result={"fuel__callouts__plan__max": 120}), ["wasmq_form"], + statement("ok", ["rule 14: " + WASMQ_RULE_WORDS[14]])), + ("a call fault past its callout's max", wasmq_job( + fault=True, envelope='"status":"fault","method":"stabilizer",', + result={"fuel__callouts__decode64__max": 399}), ["wasmq_form"], + statement("fault", [_rule_words(18, {"status": "fault"})])), + ("a fault's unsealed status says done: valid, one note", wasmq_job(fault=True), [], + statement("fault", notes=['the host\'s unsealed status says "done"' + sealed_line + '"fault"'])), + ("the unsealed status and method changed", wasmq_job(envelope='"status":"failed","method":7,'), [], + statement("ok", notes=[ + 'the host\'s unsealed status says "failed"' + sealed_line + '"ok", and the body carries "done" for that', + 'the host\'s unsealed method says 7' + sealed_line + '"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' + sealed_line + '"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}), ["wasmq_form"], + statement(None, ["rule 18: the status is neither ok nor fault"], + ['the host\'s unsealed status says "done"' + sealed_line + '7, and the body carries "fault" for that'])), + ("a sealed status missing: outcome null", wasmq_job( + result={"status": _DELETE}, envelope='"status":"fault","method":"stabilizer",'), ["wasmq_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"' + sealed_line + '"provider"'])), + ) + 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"}))) + + # In every case of the table above: a wasmQ job's problems are the rules it breaks, one + # entry a rule, in order, and there are some exactly when wasmq_form is reported; + # another job states nothing. + def problem_rules(v): + return [int(p.split(":", 1)[0][len("rule "):]) for p in v["wasmq"]["problems"]] + + unstated = [] + for name, raw_case, want, rules in wasmq_cases: + v = judge(raw_case) + if rules is None: + held = "wasmq" not in v + else: + 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) + check("wasmq statement: its problems are the rules broken, in all %d cases%s" % ( + len(wasmq_cases), (": " + ", ".join(unstated)) if unstated else ""), not unstated) + 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([]) == []) + # Reported alone, a job's form is not read, and it states nothing. + duplicated_wasmq = wasmq_run.replace(b'{"status"', b'{"status":"x","status"') + check("wasmq statement: unreadable states none", judge(duplicated_wasmq) == _verdict(["unreadable"])) + check("wasmq statement: no_receipt states none", judge(changed_after( + wasmq_run, lambda job: job["result"].pop("receipt"))) == _verdict(["no_receipt"])) + check("wasmq statement: not_a_job states none", judge(changed_after( + wasmq_run, lambda job: job.update(request=[]))) == _verdict(["not_a_job"])) + check("wasmq statement: a set holding a wasmQ job states none", "wasmq" not in judge(_set_body( + 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)] @@ -2637,6 +2895,25 @@ def self_test(args): 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_job(result={"profile": "wai.quantum.wasmq/2"}) + 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") == statement("ok", ["rule 11: " + WASMQ_RULE_WORDS[11]])) + check("plant wasmq-unchecked: planted statement, no problem, its notes kept", + unchecked_statement == 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 == statement( + "fault", notes=['the host\'s unsealed status says "done"' + 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/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 49e980a92..c085cff2a 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -1500,6 +1500,28 @@ fn main() { Command::Task { cmd, args } => task::main(cmd, &args), Command::Verify { file, history: hist, bundle, json: as_json, lines } => { let bytes = read_job_bytes(file.as_deref()).unwrap_or_else(|e| die(&e)); + 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 each { + let c = job::check_bytes(line); + all &= c.valid(); + println!("{}", c.to_json()); + } + if !all { + std::process::exit(1); + } + return; + } let src = match String::from_utf8(bytes) { Ok(s) => s, Err(e) => { @@ -1512,18 +1534,6 @@ fn main() { std::process::exit(1); } }; - if lines { - let mut all = true; - for line in src.lines().filter(|l| !l.trim().is_empty()) { - let c = job::check_text(line); - all &= c.valid(); - println!("{}", c.to_json()); - } - if !all { - std::process::exit(1); - } - 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); @@ -1535,11 +1545,12 @@ fn main() { }; // 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; - let set = v["result"]["format"] == wai_quantum::engine::sets::FORMAT; // A wasmQ job is read and judged as every job is (job::check holds it to its - // profile's form); only its envelope, run again here, is its own to check. + // 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"); } @@ -1578,11 +1589,11 @@ fn main() { return; } } - if v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { + if own_verifier && 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 { + } else if own_verifier { if !variational::verify(&v) { std::process::exit(1); } @@ -1599,8 +1610,12 @@ fn main() { } said["history"] = r; } + // The envelope is run again only for a job that was judged: one refused as + // unreadable or with no receipt is reported alone (section 8.4). #[cfg(feature = "wasmq")] - if let Some(b) = &bundle { + if let Some(b) = &bundle + && c.wasmq.is_some() + { wasmq::bundle_into(&v, b, &mut said); } println!("{said}"); diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 6d926f8ff..4676fffec 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -627,7 +627,7 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli 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 job::has_own_verifier(&parsed) { if let Some(reason) = job::text_refusal(text) { return Ok(json!({ "valid": false, "reasons": [reason] })); } @@ -635,11 +635,8 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli } return job::verdict_of_text(text).or_else(|_| Ok(job::verdict_of(&job::check_text(text)))); } - 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)); + if job::has_own_verifier(given) { + return if given["result"]["format"] == wai_quantum::engine::estimate::FORMAT { wai_quantum::engine::estimate::verify(given) } else { Ok(wai_quantum::engine::variational::verify(given)) }; } job::verdict(given) } diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index b7f4aac6e..3d05ee720 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -294,7 +294,23 @@ fn print_job(body: &Value, top: usize, kind: EngineKind) { /// 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 { - let w = c.wasmq.clone().unwrap_or(Value::Null); + // 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 { + let why = match c.reasons.first().copied() { + Some("unreadable") => "its text is unreadable", + Some("no_receipt") => "its result carries no receipt", + Some("not_a_job") => "it is not a job", + Some(other) => other, + None => "it was not read", + }; + println!("result not judged: {why}, which is reported alone (the provider protocol's section 8.4)"); + if bundle.is_some() { + println!("bundle not run: the job was not judged"); + } + 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"]), diff --git a/wai/wai-quantum-desktop/src/main.rs b/wai/wai-quantum-desktop/src/main.rs index 0d35a2d97..29793cf17 100644 --- a/wai/wai-quantum-desktop/src/main.rs +++ b/wai/wai-quantum-desktop/src/main.rs @@ -1437,5 +1437,10 @@ mod tests { 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..eaa3e1848 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -219,10 +219,21 @@ def verify(job: Any) -> Verification: dict can no longer show. ``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("{"): + # Text, a byte order mark before it among what it may begin with: read as text, where + # the mark makes it unreadable, never taken for a path. + if isinstance(job, str) and job.lstrip(" \t\r\n\ufeff").startswith(("{", "[")): return Verification(_engine.verify_text(job.encode("utf-8"))) 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, and a + # number past 64 bits is a number, read as its double (section 8.2). + try: + text = json.dumps(job, ensure_ascii=False) + except RecursionError: + return Verification({"valid": False, "reasons": ["unreadable"], "key": None, "form": None, "signer": None}) + return Verification(_engine.verify_text(text.encode("utf-8"))) 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..bde1f8e62 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -572,6 +572,23 @@ 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_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-service/README.md b/wai/wai-quantum-service/README.md index 27fde1a4c..5b8a2392f 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, or has no receipt gets that reason as its verdict | | `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/src/http.rs b/wai/wai-quantum-service/src/http.rs index d69720d8b..a8cd16e97 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -563,19 +563,22 @@ async fn cancel(State(app): State, Path(id): Path) -> Response { /// 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. +/// provider's result says of itself. A body that does not read, is no job or +/// has no receipt gets that reason as its verdict. An estimate is also re-run, +/// and every resource receipt in it checked. 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") }; + // Text that does not read as a job's (§8.2), a body that is no job, a result with no + // receipt: each is a verdict with that reason alone (§8.4), as every surface gives it. + let Some(text) = std::str::from_utf8(&bytes).ok() else { return reply(200, wai_quantum::engine::job::check_bytes(&bytes).to_json()) }; + let Ok(v) = serde_json::from_str::(text) else { return reply(200, wai_quantum::engine::job::check_text(text).to_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) { + if wai_quantum::engine::job::has_own_verifier(&v) && 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 { + if wai_quantum::engine::job::has_own_verifier(&v) && 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); @@ -589,12 +592,6 @@ async fn verify(State(app): State, ConnectInfo(peer): ConnectInfo 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", "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" }, "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 } }, { @@ -828,13 +828,14 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu 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) + // The job's members as given, the host's unsealed ones among them, checked as every + // surface checks a job (`job::verdict`): what is sealed decides, and where the + // body's other members differ from it, a wasmQ job's verdict notes it. + let mut body = args.as_object().cloned().unwrap_or_default(); + body.remove("job"); + let v = wai_quantum::engine::job::verdict(&Value::Object(body)).unwrap_or_else(|e| json!({ "valid": false, "error": e })); + 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 { format!("not valid: {}", v["reasons"]) }; + tool_result(text, v, false) } "quantum_service_info" => { let c = &app.service.config; @@ -1169,7 +1170,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 1d892a7a3..bcc9e4434 100644 --- a/wai/wai-quantum-service/tests/verify.rs +++ b/wai/wai-quantum-service/tests/verify.rs @@ -96,3 +96,46 @@ fn the_service_gives_every_receipt_the_verdict_the_corpus_gives_it() { let _ = std::fs::remove_dir_all(&dir); 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")); +} diff --git a/wai/wai-quantum-views/src/lib.rs b/wai/wai-quantum-views/src/lib.rs index 224f83430..324e6d5f4 100644 --- a/wai/wai-quantum-views/src/lib.rs +++ b/wai/wai-quantum-views/src/lib.rs @@ -50,15 +50,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 +142,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. diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index 7cfddfb3e..d4ffb3b2d 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,18 @@ 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. pub fn verdict_of_text(text: &str) -> Result { + let c = check_text(text); + if alone(&c) { + return Ok(verdict_of(&c)); + } 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) }); + return Ok(of_a_set(&job, &c)); } - verify(&job)?; - Ok(verdict_of(&check_text(text))) + Ok(verdict_of(&c)) } /// A check as a surface gives it. @@ -237,6 +248,14 @@ pub fn check_text(text: &str) -> Check { c } +/// 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) +} + /// Whether a job is a set's (§8.6). fn is_set(job: &Value) -> bool { job["result"]["format"] == super::sets::FORMAT diff --git a/wai/wai-quantum/src/engine/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 7b11e7185..0c87479b7 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -54,6 +54,9 @@ pub const MAX_CODE_BYTES: usize = 1 << 19; 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 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 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. @@ -72,7 +75,9 @@ pub fn body_status(result: &Value) -> &'static str { /// 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 { - v.as_u64().or_else(|| v.as_f64().filter(|x| x.fract() == 0.0 && (0.0..=MAX_EXACT as f64).contains(x)).map(|x| x as u64)) + // 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. @@ -103,14 +108,36 @@ fn members(v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mu } /// The first number in `v` past [`MAX_EXACT`] in size, by its path: readers of a receipt -/// would not all hold it. -fn past_exact(v: &Value, path: &str) -> Option { - match v { - Value::Number(n) => n.as_f64().is_some_and(|x| x.abs() > MAX_EXACT as f64).then(|| path.to_string()), - Value::Array(a) => a.iter().enumerate().find_map(|(i, x)| past_exact(x, &format!("{path}[{i}]"))), - Value::Object(m) => m.iter().find_map(|(k, x)| past_exact(x, &format!("{path}.{k}"))), - _ => None, +/// 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. @@ -201,9 +228,8 @@ fn contract_form(c: &Value, out: &mut Vec) { if run["state"] != "shot" && run["state"] != "job" { problem(format!("its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); } - let most = (MAX_HOST_WORK / FUEL_WORK) as u64; for b in ["per_call", "per_job", "per_instance"] { - if !whole(&run["fuel"][b], 0, MAX_EXACT.min(most)) { + if !whole(&run["fuel"][b], 0, MAX_BUDGET) { problem(format!("its fuel {b} is {}, past what a run may be given", run["fuel"][b])); } } @@ -219,8 +245,8 @@ pub fn problems(req: &Value, r: &Value) -> Vec { o.shift_remove("receipt"); } for (what, value) in [("request", req), ("result", &sealed)] { - if let Some(at) = past_exact(value, "") { - out.push(format!("the {what}'s `{}` is past 2^53 - 1", at.trim_start_matches('.'))); + if let Some(at) = past_exact(value) { + out.push(format!("the {what}'s `{at}` is past 2^53 - 1")); } } members(req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); @@ -358,12 +384,21 @@ pub fn problems(req: &Value, r: &Value) -> Vec { None if is_call => out.push("a call's fault that names no callout".into()), _ => {} } + } match f.get("budget") { Some(x) if reason != Some("fuel_exhausted") => out.push(format!("a budget, {x}, on a fault of {}", f["reason"])), Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), None if reason == Some("fuel_exhausted") => out.push("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!("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!("a call of {name:?} that stopped after {} fuel, past the most one of its calls used, {}", f["fuel"], m["max"])), + Some(_) => {} + } } _ => out.push(format!("status {}", r["status"])), } @@ -446,7 +481,9 @@ mod tests { #[test] fn a_number_is_read_by_its_value() { - 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)] { + // 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}"); } } @@ -478,6 +515,52 @@ mod tests { 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"); + job["request"]["k".repeat(1 << 20)] = json!(vec![0; 1 << 19]); + let start = std::time::Instant::now(); + let p = problems(&job["request"], &job["result"]); + assert!(start.elapsed().as_secs() < 10, "{:?}", 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"); + for at in [&mut job["request"]["wasmq"]["run"]["fuel"], &mut job["result"]["fuel"]["budget"]] { + at["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:?}"); + } + + /// 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"]["used"] = json!(0); + none["fault"]["fuel"] = json!(0); + 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": 400, "max": 200 }); + 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() { diff --git a/wai/wai-quantum/src/qasm/elab.rs b/wai/wai-quantum/src/qasm/elab.rs index 363372d83..6f50104bd 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -1891,7 +1891,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)); } diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index f45eeb5b7..4cc44d5bc 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -17,7 +17,7 @@ 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::{FUEL_WORK, MAX_CODE_BYTES, MAX_ENVELOPE_BYTES, MAX_EXACT, MAX_HOST_WORK, MAX_PROGRAM_BYTES, MAX_QUBITS, MAX_SHOTS, METHOD}; +pub use wai_quantum::engine::wasmq::{FUEL_WORK, MAX_BUDGET, MAX_CODE_BYTES, 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"; @@ -190,6 +190,13 @@ pub fn check(b: &Bundle) -> Result { 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"))); } diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs index 3cf551d0b..bff55d6ab 100644 --- a/wai/wai-wasmq/tests/review.rs +++ b/wai/wai-wasmq/tests/review.rs @@ -180,6 +180,41 @@ fn a_job(module: &str, per_call: u64) -> (wai_wasmq::Opened, Value) { 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); + let sets: [(&str, fn(&mut wai_wasmq::envelope::Bundle, u64)); 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. diff --git a/wai/wai-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index 89ff02516..f9ef2e96d 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -162,6 +162,11 @@ fn more_runs() -> Vec<(&'static str, Bundle)> { 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 } diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index b3990260d..54b802b57 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) @@ -797,6 +799,11 @@ mod tests { 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 -- 2.54.0 (Apple Git-157) From 912d2382a6ec6466e588f7a984ceadee12b2e285 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 12:42:15 -0500 Subject: [PATCH 11/17] wasmQ round three: every review finding, each with a test that fails first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader's loop ranges are made as reached on main (2c8044b1a), merged here. Beside it: - The form (engine::wasmq): rule 1's walk is linear however long a member's name; a whole number past 2^53 - 1 is no count however a reader holds it; a stopped call is one of the calls made (its callout's fuel counts it, and its largest call is at least what it used); a member is present whatever its value, null among them. - Writer and verifiers agree: each budget is at most 2^39 when packed (W017, MAX_BUDGET in the engine, read by both); the program's qubits are defined for physical qubits and the bits' order for several registers, pinned by a new vector (80 vectors, digest bcc36fe0…); a hybrid reading's refusal for controls offers no method it cannot use. - One verdict for every body on every surface: unreadable text, no job and no receipt are verdicts with that reason alone from job::verdict and verdict_of_text, so the command line, both MCP tools, the service (a 200 verdict, not a 400), Python, the desktop app, a page and a view agree; the service's tool reads the body it is given; Python judges a dict as its text and reads a marked string as text; job::has_own_verifier routes a wasmQ job to its own form whatever else its result names. - The command line: --lines reads each line on its own (a line not UTF-8, or blank, is its own unreadable; a file of no line is refused); verify says a job it did not judge was not judged, runs no envelope for it, and --json --bundle keeps its schema. - The second verifier (written from the text by readers who had not seen the reference): the stopped-call clause and the wasmq statement beside each verdict. - The differential's rules stage: 67 cases at the bounds of the form's rules, sealed again, each verdict written by hand; it catches all 25 of the review's verdict-changing defects of the second verifier and each weakening of the engine's checks. - The text: which surfaces read text and which a parsed job; fuel.callouts lists what was called; a present member; the budget bound; the qubits; --lines; what is reported alone. Co-Authored-By: Claude Opus 5.5 --- ...26-11-05-wasmq-hybrid-quantum-classical.md | 22 ++++ wai/quantum-conformance/receipts/README.md | 10 ++ wai/quantum-conformance/receipts/generate.py | 11 ++ ...crafted-wasmq-call-fault-past-largest.json | 98 ++++++++++++++++ .../crafted-wasmq-call-fault-uncounted.json | 92 +++++++++++++++ .../jobs/crafted-wasmq-fault-null.json | 111 ++++++++++++++++++ .../receipts/manifest.json | 24 ++++ .../wasmq/envelopes/physical-qubits.wai | Bin 0 -> 832 bytes wai/quantum-conformance/wasmq/vectors.json | 14 +++ wai/wai-quantum-cli/docs/llms-full.txt | 2 + wai/wai-quantum-cli/docs/service-mcp.json | 6 + wai/wai-quantum-cli/src/mcp.rs | 4 +- wai/wai-quantum-cli/tests/verify.rs | 54 +++++++++ wai/wai-quantum-cli/tests/wasmq.rs | 28 +++++ wai/wai-quantum-desktop/src/main.rs | 3 +- wai/wai-quantum-py/tests/test_engine.py | 5 +- wai/wai-quantum/src/engine/job.rs | 13 ++ wai/wai-quantum/src/engine/wasmq.rs | 13 +- wai/wai-wasmq/mutations.py | 21 +++- wai/wai-wasmq/tests/review.rs | 3 +- 20 files changed, 520 insertions(+), 14 deletions(-) create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-null.json create mode 100644 wai/quantum-conformance/wasmq/envelopes/physical-qubits.wai diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index c88a7c6e5..607450111 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -70,6 +70,28 @@ 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. +- **The differential's rules stage**: each rule of the form at its bound, sealed again, + each verdict written by hand; with it the differential catches every one of the 25 + single-rule defects of the second verifier that change a verdict. - **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 diff --git a/wai/quantum-conformance/receipts/README.md b/wai/quantum-conformance/receipts/README.md index 6936c8afd..f7e7be7fa 100644 --- a/wai/quantum-conformance/receipts/README.md +++ b/wai/quantum-conformance/receipts/README.md @@ -50,6 +50,16 @@ 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. + 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 402d1661a..51d82bd8b 100644 --- a/wai/quantum-conformance/receipts/generate.py +++ b/wai/quantum-conformance/receipts/generate.py @@ -740,6 +740,17 @@ def cases(): add("wasmq-modules-other", edited(wrun, lambda b: b["result"]["modules"][0].update(blake3="ab" * 32)), ["wasmq_form"], "a wasmQ result naming a module the contract does not pin (rule 16)") add("wasmq-transcript-upper", edited(wrun, lambda b: b["result"].update(transcript_blake3=b["result"]["transcript_blake3"].upper())), ["wasmq_form"], "a wasmQ transcript digest in upper case (rule 12)") add("wasmq-first-form", seal(wrun, form="first"), ["wasmq_form"], "a wasmQ job sealed in the first form, older than the profile: the seal holds, and no host wrote it (rule 10)") + def uncounted(b): + b["result"].update(callouts=0) + b["result"]["fuel"].update(callouts={}, used=0) + b["result"]["fault"].update(fuel=0) + add("wasmq-call-fault-uncounted", edited(wfault, uncounted), ["wasmq_form"], "a call that stopped the run, which the fuel counts no call of: the stopped call is one of the calls made (rule 18)") + + def past_largest(b): + b["result"].update(callouts=2) + b["result"]["fuel"]["callouts"]["f"].update(calls=2, fuel=400, max=200) + add("wasmq-call-fault-past-largest", edited(wfault, past_largest), ["wasmq_form"], "a call that stopped after 400 fuel, where the most one call of it used is 200 (rule 18)") + add("wasmq-fault-null", edited(wrun, at(R + ("fault",), None)), ["wasmq_form"], "an ok result with a fault member whose value is null: a member is present whatever its value (rule 18)") respelt = copy.deepcopy(wrun) respelt["request"]["wasmq"]["run"]["shots"] = Raw("32.0", 32) respelt["result"]["shots"] = Raw("3.2e1", 32) diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json new file mode 100644 index 000000000..503eea948 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 2, + "fuel": { + "used": 400, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 2, + "fuel": 400, + "max": 200 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "4efacaf3bd272c580a511f4ab95977c8c2572c88a9e8236396d61df2d4addb18", + "signature": "f45ce5ed9e8124aa67f5415bbc1cc90c9a93216b8a5ac9f21f4cffd01e4c8eb0e9b137ed56281f644a685ce8527783387961a903bb956befb256508cded31001" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json new file mode 100644 index 000000000..5e77b4141 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json @@ -0,0 +1,92 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 0, + "fuel": { + "used": 0, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": {} + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 0, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "bf37d322ec865f42ea37bb66c1649b6f914b5a7a65f4774cbb30c72570291f24", + "signature": "528b60aa37d5b7f9ac469c695363c79c205c80c219a36bea34d2e5c7ed5c82ed8214045df50da5453c67b9ec47ac3809c2b7f6d059ea468c4c204e1c81d31407" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-null.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-null.json new file mode 100644 index 000000000..93c875921 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-null.json @@ -0,0 +1,111 @@ +{ + "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" + }, + "fault": null, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "354d530ddc5ecbec7050d0d7c398342b5bf4ca203756a6110762059337d9236b", + "signature": "464dfe6c951e6260f6dca0c835a798e303b1f2b8d9bda4cf6ef40b59acfc532925f8cd55c22b246b652f2e98f2aaf2096bdbed8c4b14b6dd8638046afab46603" + } + } +} diff --git a/wai/quantum-conformance/receipts/manifest.json b/wai/quantum-conformance/receipts/manifest.json index 3fe670321..ecc929c39 100644 --- a/wai/quantum-conformance/receipts/manifest.json +++ b/wai/quantum-conformance/receipts/manifest.json @@ -1295,6 +1295,30 @@ "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 200 (rule 18)" + }, + { + "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-respelt.json", "reasons": [], 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 0000000000000000000000000000000000000000..47f0bdc135a02060c47de1fc757eb9d3bd24a40a GIT binary patch literal 832 zcmZWnO>fjN5KRgcY55b8#i5m!CC+ztRiqv`a41l5=%HE_+Zk_76DR9+)Gbl^=Q#79 z7;g#)kxORYyqS5PnXB^;^oQqp1G#64ED2qNWKJ5W6hn_I&CCM=N?Xtt5d&SV1he2+ zZ3Xp>fo-o0G>7;=1nU@AwXzS$#+r+5&$R8E#XZ|KTg;NIt~nETWU2eQ9_K`~x>8oP zTImg04&+Yh>X7UB7`M!90DBK)+jC`iWEs!N?zVMSf))VM^KslBdEV9K1^wx}=Ll@m z_KtHs5ZHIEaq=Y!fV~BypB#KcUjtc&CRawZ=7DyWX8? zrE*ICAKF`bV7D8?8aIYN(n293mPsgS0SO3^vN8*0LRnc9vW!`gq!EpxG!!p ziZNbAB1^M8P5w%w5p_%M4g(uToVw|8L<5mM)b8Jbxp5;UOBzleN=8iSon=~}?%#Mg z1-`W$v7F{*k)gf>OEzF{Ten(FaVPQ}3@Uiq-#;t0?!b_TOD5y^>GS)Km*-zDf_M?G zW=e~?uOPrG)dqRH1tMfMo7Ni06TFdYx;_owybC6CHJknco2+OZ@YQS|ypGUEYH$`b uz;?ZX1H{mmC+u`J^YCQxm^}a3|LQ;bkI#;L@8t{Mm!3B}_Mdx!_v#lEE%?y@ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/wasmq/vectors.json b/wai/quantum-conformance/wasmq/vectors.json index 9d7a550cb..c428361c5 100644 --- a/wai/quantum-conformance/wasmq/vectors.json +++ b/wai/quantum-conformance/wasmq/vectors.json @@ -608,6 +608,20 @@ "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": [ diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 44a59254d..2f623edc9 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -709,6 +709,8 @@ 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) - `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 diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 5b20fc8ca..c0d2d431d 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -31819,6 +31819,12 @@ }, "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" } } }, diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 4676fffec..835a2e305 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -1652,7 +1652,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/tests/verify.rs b/wai/wai-quantum-cli/tests/verify.rs index a9ff005ae..957783dee 100644 --- a/wai/wai-quantum-cli/tests/verify.rs +++ b/wai/wai-quantum-cli/tests/verify.rs @@ -224,3 +224,57 @@ fn a_wasmq_job_gets_the_corpus_s_verdict_from_every_reader() { 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); +} diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index 4d54cee0c..ac762618d 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -432,3 +432,31 @@ fn verify_refuses_a_file_past_any_job_s_size_before_it_is_read() { let o = run(&d, &["verify", path.to_str().unwrap()]); assert!(!o.status.success() && text(&o).contains(&format!("{} holds more than", path.display())), "{}", 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 29793cf17..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] diff --git a/wai/wai-quantum-py/tests/test_engine.py b/wai/wai-quantum-py/tests/test_engine.py index bde1f8e62..dde09378e 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(): diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index d4ffb3b2d..ee95d88e6 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -584,6 +584,19 @@ pub fn answer(request: &Value, outcome: Result) -> Result(text) { + assert_eq!(verdict(&parsed).unwrap_or_else(|e| panic!("{text}: {e}"))["reasons"], json!([want]), "{text}"); + } + } + } + #[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/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 0c87479b7..6009bb3fe 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -384,7 +384,6 @@ pub fn problems(req: &Value, r: &Value) -> Vec { None if is_call => out.push("a call's fault that names no callout".into()), _ => {} } - } match f.get("budget") { Some(x) if reason != Some("fuel_exhausted") => out.push(format!("a budget, {x}, on a fault of {}", f["reason"])), Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), @@ -399,6 +398,7 @@ pub fn problems(req: &Value, r: &Value) -> Vec { Some(m) if count(&m["max"]).zip(used).is_none_or(|(max, u)| u > max) => out.push(format!("a call of {name:?} that stopped after {} fuel, past the most one of its calls used, {}", f["fuel"], m["max"])), Some(_) => {} } + } } _ => out.push(format!("status {}", r["status"])), } @@ -520,10 +520,12 @@ mod tests { #[test] fn a_long_name_over_many_numbers_is_read_in_a_line() { let mut job = corpus("cli-wasmq-run.json"); - job["request"]["k".repeat(1 << 20)] = json!(vec![0; 1 << 19]); + // 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() < 10, "{:?}", start.elapsed()); + assert!(start.elapsed().as_secs() < 5, "{:?}", start.elapsed()); assert_eq!(p.len(), 1, "the one member the request does not define: {p:?}"); } @@ -531,9 +533,8 @@ mod tests { #[test] fn a_budget_past_two_to_the_53_is_no_budget() { let mut job = corpus("cli-wasmq-run.json"); - for at in [&mut job["request"]["wasmq"]["run"]["fuel"], &mut job["result"]["fuel"]["budget"]] { - at["per_call"] = json!(10000000000000001u64); - } + 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:?}"); } diff --git a/wai/wai-wasmq/mutations.py b/wai/wai-wasmq/mutations.py index 250d419c3..66a783862 100644 --- a/wai/wai-wasmq/mutations.py +++ b/wai/wai-wasmq/mutations.py @@ -22,7 +22,7 @@ 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). The tests are wasmQ's, but for a file of wai-quantum tested by +# 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 = [ @@ -116,6 +116,20 @@ M = [ # 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"]), @@ -125,7 +139,7 @@ M = [ ] -def run(name, crate, path, old, new, args, tests): +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: @@ -133,7 +147,8 @@ def run(name, crate, path, old, new, args, tests): # 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. - tested = crate if "--lib" in args or "--features" in args or crate == CLI else WASMQ + # 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 diff --git a/wai/wai-wasmq/tests/review.rs b/wai/wai-wasmq/tests/review.rs index bff55d6ab..0aa849344 100644 --- a/wai/wai-wasmq/tests/review.rs +++ b/wai/wai-wasmq/tests/review.rs @@ -198,7 +198,8 @@ fn a_refusal_for_controls_offers_no_method_a_hybrid_program_cannot_use() { 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); - let sets: [(&str, fn(&mut wai_wasmq::envelope::Bundle, u64)); 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)]; + 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); -- 2.54.0 (Apple Git-157) From 5686c53a52e9141c8a47eee692f68c447e419ef4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 08:15:42 -0500 Subject: [PATCH 12/17] wasmQ: a fault outside a call is held to its instance budget by a test of its own The stopped-call rule and rule 14 together hold a call fault to the call budget, so the sweep tests no longer catch the phase-budget check removed; an init fault past per_instance does. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/engine/wasmq.rs | 19 +++++++++++++++++++ wai/wai-wasmq/mutations.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/wai/wai-quantum/src/engine/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 6009bb3fe..7d704f67a 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -539,6 +539,25 @@ mod tests { 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); + 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 its 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. diff --git a/wai/wai-wasmq/mutations.py b/wai/wai-wasmq/mutations.py index 66a783862..225d72d59 100644 --- a/wai/wai-wasmq/mutations.py +++ b/wai/wai-wasmq/mutations.py @@ -107,7 +107,7 @@ M = [ ("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 {", ["--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"]), -- 2.54.0 (Apple Git-157) From a612c305b868d5d4803b2274f10e2cde88ed4222 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 15:31:20 -0500 Subject: [PATCH 13/17] wasmQ round four: every tie the writer keeps is a rule, weakenings drive the evidence, one answer for every kind of job Rules from the writer. Section 6.5 gains rules 20 to 31, each a property the runner guarantees between members: a returned call used 1 to the most one call used and the stopped one the fault's fuel; each shot's instances passed their priors; a call's fault is in its callout's module, a prior's in a module that has one, a bad_result in a call or a prior; a lifecycle fault is within the instances' fuel, in shot 0 for one instance a run, and before any call in shot 0; a fuel fault used its whole limit; and section 4.8's products and sums over what a receipt shows (a shot's bits, the shots' bits, the work, the frames). Section 6.5 says what it leaves free and why. Rules 6, 8, 14 and 18 no longer repeat what other rules hold. Every problem names its rule. The corpus holds a receipt for each of rules 2 to 31 that breaks that rule alone, and the two hand-written expectations that called contradictions valid are flipped; SPEC-GAP W9 is gone. Evidence the verifiers agree. tools/wasmq_mutants.py weakens each rule in each verifier from its code (a check dropped, a comparison moved by one, a count or a digest read loosely, null read as absent and absent as null), and the differential must catch each. The differential compares the rules each verifier states broken on every case; its rules stage holds every member of each genuine job given a value of another kind; and a labelled stage runs the second verifier's own hand-labelled cases through the reference. Nightly in CI. The outcome. Section 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 with no bit in a program that calls externs is refused (Q324). Three vectors pin them. One answer. An estimate or an algorithm's answer with no receipt, or that is no job, is that reason alone on every surface, and is run again only once its receipt holds (job::answer_of); the service runs an algorithm's answer again, its tool alike for text, a job and members. POST /v1/verify and /mcp read 64 MiB, and a longer job is too_large; a job 127 deep reaches its verdict through a view or an MCP call; verify prints every verdict; Python gives a verdict for every str and dict. Text and API. The OpenAPI document says 200 for every body; Q209 keeps its released meaning and nesting past 128 is Q130; Breaking lines for the 200, the nesting refusal and Python's str. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-cli.yml | 23 + ...26-11-05-wasmq-hybrid-quantum-classical.md | 54 +- wai/extensions/quantum-providers.md | 6 +- wai/extensions/quantum-wasmq.md | 139 +- wai/quantum-conformance/receipts/README.md | 20 +- wai/quantum-conformance/receipts/generate.py | 69 +- .../crafted-wasmq-bad-result-in-init.json | 90 ++ ...crafted-wasmq-call-fault-other-module.json | 113 ++ ...crafted-wasmq-call-fault-past-largest.json | 10 +- .../crafted-wasmq-call-fault-uncounted.json | 10 +- .../crafted-wasmq-call-past-per-call.json | 110 ++ .../jobs/crafted-wasmq-callouts-twice.json | 115 ++ .../jobs/crafted-wasmq-calls-of-no-fuel.json | 110 ++ .../crafted-wasmq-contract-member-added.json | 111 ++ .../jobs/crafted-wasmq-contract-profile.json | 110 ++ .../jobs/crafted-wasmq-fault-unnamed.json | 7 +- ...afted-wasmq-first-instance-after-call.json | 96 ++ .../crafted-wasmq-frames-past-envelope.json | 96 ++ .../crafted-wasmq-fuel-fault-under-limit.json | 98 ++ ...rafted-wasmq-job-state-later-instance.json | 98 ++ ...d-wasmq-lifecycle-fuel-past-instances.json | 98 ++ .../jobs/crafted-wasmq-module-empty.json | 110 ++ .../crafted-wasmq-prior-fault-no-prior.json | 90 ++ .../jobs/crafted-wasmq-priors-unpassed.json | 110 ++ .../jobs/crafted-wasmq-program-empty.json | 110 ++ .../crafted-wasmq-request-member-added.json | 111 ++ .../jobs/crafted-wasmq-result-seed.json | 110 ++ .../jobs/crafted-wasmq-results-past-bits.json | 100 ++ .../crafted-wasmq-run-budget-not-spent.json | 101 ++ .../jobs/crafted-wasmq-work-past-bound.json | 110 ++ .../receipts/manifest.json | 170 ++- .../tools/receipt_differential.py | 438 +++++- .../tools/receipt_verify.py | 1354 ++++++++++++----- .../tools/wasmq_mutants.py | 536 +++++++ .../wasmq/envelopes/bits-nothing-writes.wai | Bin 0 -> 820 bytes .../envelopes/block-bit-not-in-outcome.wai | Bin 0 -> 911 bytes .../refuse/program-measure-with-no-bit.wai | Bin 0 -> 804 bytes wai/quantum-conformance/wasmq/vectors.json | 32 + wai/wai-quantum-cli/docs/llms-full.txt | 10 +- wai/wai-quantum-cli/docs/local-mcp.json | 14 + wai/wai-quantum-cli/docs/service-mcp.json | 14 +- wai/wai-quantum-cli/src/lab.rs | 6 + wai/wai-quantum-cli/src/main.rs | 223 +-- wai/wai-quantum-cli/src/mcp.rs | 30 +- wai/wai-quantum-cli/src/wasmq.rs | 12 +- wai/wai-quantum-cli/tests/verify.rs | 94 +- wai/wai-quantum-cli/tests/wasmq.rs | 15 +- wai/wai-quantum-py/python/wai_quantum/_api.py | 42 +- wai/wai-quantum-py/tests/test_engine.py | 24 + wai/wai-quantum-service/README.md | 2 +- wai/wai-quantum-service/src/discovery.rs | 14 +- wai/wai-quantum-service/src/http.rs | 96 +- wai/wai-quantum-service/src/mcp.rs | 62 +- wai/wai-quantum-service/tests/verify.rs | 94 ++ wai/wai-quantum-views/src/lib.rs | 20 +- wai/wai-quantum/Cargo.toml | 2 +- wai/wai-quantum/docs/qasm-diagnostics.md | 4 +- wai/wai-quantum/docs/qasm-feature-matrix.md | 4 +- wai/wai-quantum/schemas/algorithms.v1.json | 7 + wai/wai-quantum/schemas/estimate.v1.json | 7 + wai/wai-quantum/src/engine/estimate/mod.rs | 5 + wai/wai-quantum/src/engine/job.rs | 168 ++ wai/wai-quantum/src/engine/wasmq.rs | 507 ++++-- wai/wai-quantum/src/qasm.rs | 2 +- wai/wai-quantum/src/qasm/diag.rs | 4 +- wai/wai-quantum/src/qasm/elab.rs | 5 + wai/wai-quantum/src/qasm/lang_tests.rs | 23 + wai/wai-quantum/src/qasm/parse.rs | 2 +- wai/wai-quantum/tests/wasmq_verdict.rs | 2 +- wai/wai-wasmq/src/envelope.rs | 13 +- wai/wai-wasmq/tests/vectors.rs | 14 + 71 files changed, 5769 insertions(+), 837 deletions(-) create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-bad-result-in-init.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-other-module.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-past-per-call.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-callouts-twice.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-of-no-fuel.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-member-added.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-profile.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-instance-after-call.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-frames-past-envelope.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-fault-under-limit.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-job-state-later-instance.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-lifecycle-fuel-past-instances.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-module-empty.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-prior-fault-no-prior.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-priors-unpassed.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-program-empty.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-request-member-added.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-seed.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-results-past-bits.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-run-budget-not-spent.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-work-past-bound.json create mode 100644 wai/quantum-conformance/tools/wasmq_mutants.py create mode 100644 wai/quantum-conformance/wasmq/envelopes/bits-nothing-writes.wai create mode 100644 wai/quantum-conformance/wasmq/envelopes/block-bit-not-in-outcome.wai create mode 100644 wai/quantum-conformance/wasmq/refuse/program-measure-with-no-bit.wai diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml index 4025287b1..acf09d0a7 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -39,11 +39,15 @@ on: - '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/**' @@ -59,6 +63,7 @@ on: - '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' @@ -203,6 +208,24 @@ jobs: 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 diff --git a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index 607450111..c04e887fb 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -19,8 +19,9 @@ 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 (`Q209`) and its - bounds for programs from others (`Q323`) hold for every reading of a program. + finishes on every host, a 32-bit one among them. The reader's nesting bound (`Q130`, a code of + its own, counted from 1 at the top level) and its bounds for programs from others (`Q323`) hold + for every reading of a 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 @@ -58,14 +59,16 @@ 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 nineteen numbered rules hold the contract, the result, the fuel, + quantum-wasmq §6.5, whose thirty-one 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 fourteen crafted from them, twelve breaking one rule each and two the same + in a prior) and thirty-eight crafted from them: thirty-five 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), one 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 @@ -89,9 +92,46 @@ 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. -- **The differential's rules stage**: each rule of the form at its bound, sealed again, - each verdict written by hand; with it the differential catches every one of the 25 - single-rule defects of the second verifier that change a verdict. +- **Every tie the writer keeps is a rule.** Rules 20 to 31 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; each shot's instances passed their priors; 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 within the instances' fuel, in shot 0 for one instance a run, and before any call + in shot 0; a fuel fault used its whole limit; and §4.8's products and sums over what a + receipt shows (a shot's bits, the shots' bits, the work, the frames). §6.5 says what it + leaves free, and why. 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. `POST /v1/verify` and `/mcp` read 64 MiB; a longer job is + `too_large`, a new reason of §8.4 reported alone, as is a file past the 1 GiB `verify` + reads. A job as deep as a reader takes reaches its verdict through a view's request and an + MCP call, and a parsed job deeper than that is `unreadable`. `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 64 MiB + (`too_large`), where it answered 400 for the first three; a client reads `valid` and + `reasons`. Its OpenAPI document says so. +- **Breaking:** every reading of a program refuses statements, operators and brackets nested + deeper than 128 with `Q130`, where earlier versions read them until a reader's stack ran + out (about 1000 deep on an 8 MiB stack, far less on a small one): write a deep expression + as named `const` values, or nest fewer blocks. +- **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 diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index bfd0097f3..ecdbe2075 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's text is longer than the surface reads, which it states with the verdict (`read_at_most`, in bytes): it read none of it. Reported alone. `wai-quantum verify` reads 1 GiB, more than any job holds; the job service's `POST /v1/verify` reads 64 MiB | | `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 | @@ -744,7 +745,10 @@ receipt's text: the job service over HTTP and as a tool, the command line's tool desktop app and a browser tab. A surface given a job already parsed into objects (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`). The Python package judges a dict as the text it writes +keeps (`inexact_number`). A job parsed deeper than a text is read (127) is +`unreadable` there too, and a message that carries a job (a tool's call, a view's +request) is read with room for itself around a job as deep as that, so that the job +reaches its verdict. 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 diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index f29d72e88..07139301a 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -104,7 +104,9 @@ its length. A `name` is for people. `method` is `stabilizer`, the one method of 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, the most fuel a run may be given (§4.8). Every module is named by +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. @@ -371,7 +373,8 @@ fuel counts: reading the program, its steps on the tableau, a fresh instance's m 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, and nothing here is in a receipt. +engine. A receipt shows part of what these bounds count, and §6.5 holds a job to that part +(rules 29 to 31). **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 @@ -380,7 +383,7 @@ a short program cannot make a reader hold or do more than a host has before it i | bound | | |---|---| | its text | 1 MiB (a reader holds some eighty bytes of tokens for each) | -| nesting of statements, operators and brackets | 128 (`Q209`): a reader's stack | +| nesting of statements, operators and brackets | 128 (`Q130`): a reader's stack. A statement at the top level is at depth 1; each statement in a block, and each operand of an operator or inside a bracket, is one deeper than what holds it | | 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 | @@ -391,8 +394,9 @@ Past one, the reader's code is `Q323`, or `Q111` for the qubits. **A run ends**: 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 of a shot (all the program's bits, its blocks' -own among them) are at most 2^28 (`W017`). With the most memory (§4.7), the largest +**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`). @@ -544,10 +548,15 @@ takes the stream's next value `x`, and is 1 when `x < 2^63`. A `reset` measures 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, 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 program that measures nothing has as its outcome the bits its -calls write: no qubit is read for it. +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 @@ -654,16 +663,17 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 from 1 to 2^19, and `priors` an array each of whose priors has exactly the members - `name` (a string), `blake3` (a digest) and `bytes` (a count of at most 2^27). The - modules' `bytes` that are counts add up to at most 2^19. + 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 - of at most 2^39 (§4.8). + `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`. @@ -679,7 +689,8 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 of at most `calls` × `max`. + 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. @@ -696,8 +707,9 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F `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 of at most `F`'s `used` and of at most its phase's budget (the budget - `per_call` in a call, `per_instance` otherwise, 0 when that is not a count); it has a + 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 @@ -707,9 +719,90 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 31 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. Its `fuel` less `s` × the + fault's `fuel` (0 when that is not a count), 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. `calls`, `fuel` and `max` are counts. +21. `F`'s `instances` is a count of at least `W` × `P`. `P` is the sum, over every prior + of every one of `C`'s modules, of 2 and the prior's `bytes` (0 when that is not a + count): passing a prior calls `cabi_realloc` and `wasmq-prior`, each at least 1, and + charges its bytes (§5.2, §5.3). `W`, the shots whose instances were all made, is + `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. +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 of at most `F`'s `instances` (a count): what a + lifecycle call used is part of the instances' fuel. +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). +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 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). +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` + 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; and each sum + is over `C`'s modules, `p` a module's number of priors and `b` its `bytes`. 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 and its fuel. +31. The program's `bytes`, each module's `bytes` and each prior's `bytes`, added (each 0 + when it is not a count), are less than 2^27: the frames an envelope of at most + 128 MiB holds (§2.3). + +**What the rules leave free, and why.** Everything else the writer fixes depends on what +no receipt holds, the program's text, the modules' code, the priors' bytes or the run +itself, and only running the envelope again (`--bundle`) holds a job to it: + +- the `transcript_blake3`'s value, and each `blake3` against the bytes it pins: they are + digests of what is not sealed; +- `r`'s `qubits`, the outcomes' length and the number of callouts 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 a 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`: only its code shows it. So a fault in + phase `seed` or `init` may name any module, and the instances' fuel is bounded above by + rule 17 and below only by its priors' part and a lifecycle fault's own fuel (rules 21 + and 25); +- the fuel of each call beyond rules 14, 18, 20 and 28, and a fault's fuel beyond rules + 18, 20, 25 and 28: 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 (steps, gates, 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 31 take what a receipt shows, and a job a writer can write + never breaks them. + Beside §8.4's reasons a verifier of the engine states, for a wasmQ job, `wasmq`: -`outcome`, the sealed result's `status`; `problems`, each rule the job breaks, in words, -the first 64 of them and then how many more; and `notes`, where the host's unsealed words +`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. @@ -719,9 +812,11 @@ note; `--json` gives the verdict as JSON. `--lines` reads a file of one job a li 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, or with no receipt is reported alone (§8.4): `verify` says that its -form was not read, and runs no envelope for it. `verify` reads at most 1 GiB, more than any job holds, and refuses a -file past it by its size, before it reads it. With `--bundle ENVELOPE.wai` it also checks +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 1 GiB, more than any job +holds, and gives a file past it `too_large` by its size, before it reads it; a file of +lines past it is refused. 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 diff --git a/wai/quantum-conformance/receipts/README.md b/wai/quantum-conformance/receipts/README.md index f7e7be7fa..706721450 100644 --- a/wai/quantum-conformance/receipts/README.md +++ b/wai/quantum-conformance/receipts/README.md @@ -58,7 +58,25 @@ and sealed again with the demo key, each with the verdict written for it by hand 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. +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), 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 diff --git a/wai/quantum-conformance/receipts/generate.py b/wai/quantum-conformance/receipts/generate.py index 51d82bd8b..3f637578c 100644 --- a/wai/quantum-conformance/receipts/generate.py +++ b/wai/quantum-conformance/receipts/generate.py @@ -733,7 +733,7 @@ def cases(): add("wasmq-fuel-unaccounted", edited(wrun, lambda b: b["result"]["fuel"].update(instances=b["result"]["fuel"]["instances"] + 1)), ["wasmq_form"], "a wasmQ run whose instances and calls do not add up to the fuel it used (rule 15)") add("wasmq-fuel-past-job-budget", edited(wfault, budget("per_job", 100)), ["wasmq_form"], "a wasmQ fault that used 400 fuel under a run's budget of 100 (rule 13)") add("wasmq-instances-past-budget", edited(wjob, budget("per_instance", 8000)), ["wasmq_form"], "one instance for the whole run, given more fuel for its seed, init and prior than three stretches of 8000 (rule 17)") - add("wasmq-fault-unnamed", edited(wfault, at(R + ("fault", "reason"), "out_of_fuel")), ["wasmq_form"], "a wasmQ fault of a reason the profile does not name (rule 18)") + add("wasmq-fault-unnamed", edited(wfault, lambda b: (b["result"]["fault"].update(reason="out_of_fuel"), b["result"]["fault"].pop("budget"))), ["wasmq_form"], "a wasmQ fault of a reason the profile does not name (rule 18)") add("wasmq-result-member-added", edited(wrun, at(R + ("note",), "a member the profile does not write")), ["wasmq_form"], "a wasmQ result with a member the profile does not write (rule 9)") add("wasmq-contract-method", edited(wrun, both("method", "statevector")), ["wasmq_form"], "a wasmQ contract and result of a method the profile does not have (rule 8)") add("wasmq-seed-past-exact", edited(wrun, both("seed", 2 ** 53)), ["wasmq_form"], "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)") @@ -742,14 +742,73 @@ def cases(): add("wasmq-first-form", seal(wrun, form="first"), ["wasmq_form"], "a wasmQ job sealed in the first form, older than the profile: the seal holds, and no host wrote it (rule 10)") def uncounted(b): b["result"].update(callouts=0) - b["result"]["fuel"].update(callouts={}, used=0) - b["result"]["fault"].update(fuel=0) + b["result"]["fuel"].update(callouts={}, instances=400) add("wasmq-call-fault-uncounted", edited(wfault, uncounted), ["wasmq_form"], "a call that stopped the run, which the fuel counts no call of: the stopped call is one of the calls made (rule 18)") def past_largest(b): b["result"].update(callouts=2) - b["result"]["fuel"]["callouts"]["f"].update(calls=2, fuel=400, max=200) - add("wasmq-call-fault-past-largest", edited(wfault, past_largest), ["wasmq_form"], "a call that stopped after 400 fuel, where the most one call of it used is 200 (rule 18)") + b["result"]["fuel"]["callouts"]["f"].update(calls=2, fuel=401, max=399) + b["result"]["fuel"]["used"] = 401 + add("wasmq-call-fault-past-largest", edited(wfault, past_largest), ["wasmq_form"], "a call that stopped after 400 fuel, where the most one call of it used is 399 (rule 18)") + + # Each rule the corpus held no receipt for, broken alone (rule 1 cannot be: every place a + # number may be has a rule of its own that holds it within 2^53 - 1). + wrunf = load("cli-wasmq-fault-run.json") + winit = load("cli-wasmq-fault-init.json") + wprior = load("cli-wasmq-fault-prior.json") + + def each(*edits): + def f(b): + for e in edits: + e(b) + return f + + def later_shot(shot, state=None, instances=1, fuel=1, callouts=None): + # The init fault moved to `shot`, after that many shots of one call each. + def f(b): + n = shot if callouts is None else callouts + if state: + b["request"]["wasmq"]["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 second_module(fault_module): + def f(b): + c = b["request"]["wasmq"] + c["modules"].append({"name": "m2", "blake3": "cd" * 32, "bytes": 57, "priors": []}) + c["callouts"].append({"extern": "g", "module": 1, "export": "g"}) + b["result"]["modules"].append({"name": "m2", "blake3": "cd" * 32}) + b["result"]["fault"]["module"] = fault_module + return f + + def shots(n): + def f(b): + b["request"]["wasmq"]["run"]["shots"] = b["result"]["shots"] = n + return f + + add("wasmq-request-member-added", edited(wrun, at(("request", "x"), 1)), ["wasmq_form"], "a wasmQ request with a member the profile does not write (rule 2)") + add("wasmq-contract-member-added", edited(wrun, at(C + ("x",), 1)), ["wasmq_form"], "a wasmQ contract with a member section 2.2 does not list (rule 3)") + add("wasmq-contract-profile", edited(wrun, at(C + ("profile",), "wai.quantum.wasmq/2")), ["wasmq_form"], "a wasmQ contract of another profile (rule 4)") + add("wasmq-program-empty", edited(wrun, at(C + ("program", "bytes"), 0)), ["wasmq_form"], "a wasmQ contract whose program is 0 bytes long (rule 5)") + add("wasmq-module-empty", edited(wrun, lambda b: b["request"]["wasmq"]["modules"][0].update(bytes=0)), ["wasmq_form"], "a wasmQ contract whose module is 0 bytes long (rule 6)") + add("wasmq-callouts-twice", edited(wrun, lambda b: b["request"]["wasmq"]["callouts"].append({"extern": "decode64", "module": 0, "export": "x"})), ["wasmq_form"], "a wasmQ contract with two callouts of one extern (rule 7)") + add("wasmq-result-seed", edited(wrun, lambda b: b["result"].update(seed=b["result"]["seed"] + 1)), ["wasmq_form"], "a wasmQ result whose seed is not its contract's (rule 11)") + add("wasmq-call-past-per-call", edited(wrun, budget("per_call", 21750)), ["wasmq_form"], "a wasmQ run whose largest call used 21751 fuel under a budget of 21750 a call (rule 14)") + add("wasmq-calls-of-no-fuel", edited(wrun, lambda b: (b["result"]["fuel"]["callouts"]["decode64"].update(fuel=0, max=0), b["result"]["fuel"].update(used=b["result"]["fuel"]["instances"]))), ["wasmq_form"], "32 calls that returned having used no fuel, where entering a function weighs 1 (rule 20)") + add("wasmq-priors-unpassed", edited(wrun, lambda b: b["result"]["fuel"].update(instances=32 * 89 - 1, used=32 * 89 - 1 + 588348)), ["wasmq_form"], "32 shots of instances passed a prior of 87 bytes in less fuel than its bytes and two calls take (rule 21)") + add("wasmq-call-fault-other-module", edited(wfault, second_module("m2")), ["wasmq_form"], "a call of a callout in the first module, whose fault names the second (rule 22)") + add("wasmq-prior-fault-no-prior", edited(winit, at(R + ("fault", "phase"), "prior")), ["wasmq_form"], "a fault while a prior is passed, in a module with no prior (rule 23)") + add("wasmq-bad-result-in-init", edited(winit, at(R + ("fault", "reason"), "bad_result")), ["wasmq_form"], "a bad_result from an init, which takes no result (rule 24)") + add("wasmq-lifecycle-fuel-past-instances", edited(winit, later_shot(1, instances=1, fuel=2)), ["wasmq_form"], "an init fault in shot 1 that used 2 fuel, where the instances used 1 (rule 25)") + add("wasmq-job-state-later-instance", edited(winit, later_shot(2, state="job")), ["wasmq_form"], "a run of one instance for the run whose instance fails in shot 2 (rule 26)") + add("wasmq-first-instance-after-call", edited(winit, later_shot(0, callouts=1)), ["wasmq_form"], "the first shot's instance failing after a call was made (rule 27)") + add("wasmq-fuel-fault-under-limit", edited(wfault, lambda b: (b["result"]["fuel"]["callouts"]["f"].update(fuel=10, max=10), b["result"]["fuel"].update(used=10), b["result"]["fault"].update(fuel=10))), ["wasmq_form"], "a call's fuel fault after 10 fuel, under its limit of 400 (rule 28)") + add("wasmq-run-budget-not-spent", edited(wrunf, budget("per_job", 60)), ["wasmq_form"], "a fault of the run's budget with 50 of its 60 used (rule 28)") + add("wasmq-results-past-bits", edited(wrunf, each(shots(2 ** 20), lambda b: b["result"].update(counts={"0" * 257: 2}))), ["wasmq_form"], "2^20 shots of 257 bits, past the 2^28 bits a run's results hold (rule 29)") + add("wasmq-work-past-bound", edited(wrun, budget("per_job", 2 ** 39)), ["wasmq_form"], "a run given 2^39 fuel beside a module's code, past the 2^42 operations a run asks (rule 30)") + add("wasmq-frames-past-envelope", edited(wprior, lambda b: b["request"]["wasmq"]["modules"][0]["priors"][0].update(bytes=2 ** 27)), ["wasmq_form"], "a prior of 2^27 bytes beside a program and a module, more than an envelope holds (rule 31)") add("wasmq-fault-null", edited(wrun, at(R + ("fault",), None)), ["wasmq_form"], "an ok result with a fault member whose value is null: a member is present whatever its value (rule 18)") respelt = copy.deepcopy(wrun) respelt["request"]["wasmq"]["run"]["shots"] = Raw("32.0", 32) diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-bad-result-in-init.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-bad-result-in-init.json new file mode 100644 index 000000000..503e7de7c --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-bad-result-in-init.json @@ -0,0 +1,90 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3", + "bytes": 50, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "shot" + } + }, + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 0, + "seed": 1, + "counts": {}, + "callouts": 0, + "fuel": { + "used": 1, + "instances": 1, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": {} + }, + "modules": [ + { + "name": "m", + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3" + } + ], + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "bad_result", + "shot": 0, + "phase": "init", + "module": "m", + "fuel": 1 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "629c657008cbdbd0a115c3242fa612ef53806f955c3118b36e099bd2fc685664", + "signature": "a6c82f983b64ca1cc4adda08f043230e35a17bb54f74644bd09ff148fc90597130da1f95752952e7d4d53cae63870dabcd6731614e3cf8abec23199f765a3106" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-other-module.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-other-module.json new file mode 100644 index 000000000..dee41cd81 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-other-module.json @@ -0,0 +1,113 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + }, + { + "extern": "g", + "module": 1, + "export": "g" + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + }, + { + "name": "m2", + "blake3": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "bytes": 57, + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 400, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 400, + "max": 400 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + }, + { + "name": "m2", + "blake3": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m2", + "fuel": 400, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "e4af5b30d391f6049cba79d7d84b0069f659afa8e6215f790314014a4cf53c25", + "signature": "ec802b9ff66aa15c36c3cf2d2ae0924d6e2928b7697fa65c612b90e90f72bc2b07102c267304ac89311e1ad8adc78dca0836bb2116fafdd223dab24315d3cb0c" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json index 503eea948..da9cdf40d 100644 --- a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-past-largest.json @@ -50,7 +50,7 @@ "counts": {}, "callouts": 2, "fuel": { - "used": 400, + "used": 401, "instances": 0, "budget": { "per_call": 400, @@ -60,8 +60,8 @@ "callouts": { "f": { "calls": 2, - "fuel": 400, - "max": 200 + "fuel": 401, + "max": 399 } } }, @@ -91,8 +91,8 @@ "alg": "ed25519", "canon": "rfc8785", "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", - "digest": "4efacaf3bd272c580a511f4ab95977c8c2572c88a9e8236396d61df2d4addb18", - "signature": "f45ce5ed9e8124aa67f5415bbc1cc90c9a93216b8a5ac9f21f4cffd01e4c8eb0e9b137ed56281f644a685ce8527783387961a903bb956befb256508cded31001" + "digest": "583cc658764532b53630e620ed79cc95ed0cf8d5ef4a54ceafea43b7e9304381", + "signature": "9341a9016bf590b7ce944cadbb070a4894b2bbb7083e3f8167109556d989979ee4087aae8de3e70649b5e8908cf9392c9b6d7fc969d1f06dc717fa24a10e730a" } } } diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json index 5e77b4141..7e03617b6 100644 --- a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-fault-uncounted.json @@ -50,8 +50,8 @@ "counts": {}, "callouts": 0, "fuel": { - "used": 0, - "instances": 0, + "used": 400, + "instances": 400, "budget": { "per_call": 400, "per_job": 17179869184, @@ -74,7 +74,7 @@ "shot": 0, "phase": "call", "module": "m", - "fuel": 0, + "fuel": 400, "callout": "f", "budget": "call" }, @@ -85,8 +85,8 @@ "alg": "ed25519", "canon": "rfc8785", "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", - "digest": "bf37d322ec865f42ea37bb66c1649b6f914b5a7a65f4774cbb30c72570291f24", - "signature": "528b60aa37d5b7f9ac469c695363c79c205c80c219a36bea34d2e5c7ed5c82ed8214045df50da5453c67b9ec47ac3809c2b7f6d059ea468c4c204e1c81d31407" + "digest": "64aa39cd10a072bd758f09c0c6b4850d266f6025d2baf16d3e5c2366c74db475", + "signature": "ed7b6d1c9603d5a9371c4d59e3355acd3d24247ff1fab0b3fc45f9c7a61d3247286833f642ede8f3cc70fd2e74438eac8ab6664e393cdd909de5233f3fb73909" } } } diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-past-per-call.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-past-per-call.json new file mode 100644 index 000000000..ad6f8e14f --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-call-past-per-call.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": 21750, + "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": 21750, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "2b2cf2d827fe92e3a3b2d637fba76aa3b0019b34f9c355639e54a5ccd6b2f45e", + "signature": "225ea523d6694ac1679a54299bfaff95df59e844ae46318c3277675aa95ac4c958695315b9a080cf81b7d92c7f08ef22c5e5c6bef59063bc959360d2663a8d09" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-callouts-twice.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-callouts-twice.json new file mode 100644 index 000000000..4d2eac2cc --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-callouts-twice.json @@ -0,0 +1,115 @@ +{ + "status": "done", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "decode64", + "extern": "decode64", + "module": 0 + }, + { + "extern": "decode64", + "module": 0, + "export": "x" + } + ], + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "bdc1785c22876018049224778e3858b046cfbaad1169317de89bc5e22d1d46cb", + "signature": "f88677c1b1e369f68e930b99cf403e6e7d88456dd11ce9d83bde8f88f60b5613eaf057e5530b2356b8ccedf462b2b4e330907ddbdf131f0f1cc5577c75d3b702" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-of-no-fuel.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-of-no-fuel.json new file mode 100644 index 000000000..65caceba8 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-of-no-fuel.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": 248032, + "instances": 248032, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "decode64": { + "calls": 32, + "fuel": 0, + "max": 0 + } + } + }, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "db162e5b9b78f0eb0ff9988798d5224d2fce210dcd1c64d7f9a297280039beae", + "signature": "c4260104d12ba3c0baad5ee973db5c8d4e9f4f2df149fbeaccf4e456e48ed75023611cffc94328e9ea5bbf469c388dd8adecca4206cc186815cb95fc5b25770e" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-member-added.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-member-added.json new file mode 100644 index 000000000..afbf618bb --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-member-added.json @@ -0,0 +1,111 @@ +{ + "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" + }, + "x": 1 + }, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "14d0220bed707653f2072608c81f8ff6681cb3ffcf0005b440818fcaa08ba680", + "signature": "805417bb8001494fbd5588ae1f4e7012e36fd1a1be0d791c1c02538050d2961de7a8def653a12fd13f737d836d1c279b9090a26ade13dd8fe65c1428de70d60c" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-profile.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-profile.json new file mode 100644 index 000000000..ef7f0d78e --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-contract-profile.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/2", + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "2daf5e56c89d2e829182a0f11eace97ae0e8870f810a1ffe952817d664307a1e", + "signature": "3d99bec66c031477ba6427f6191b482a220f058ec1cd3529ea7355f1c2929735a2061e437b6fc8de86442eb77729aaf16e15a2086e0167979a197d0b359aa70d" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json index 53e2583cb..80232ba66 100644 --- a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fault-unnamed.json @@ -81,8 +81,7 @@ "phase": "call", "module": "m", "fuel": 400, - "callout": "f", - "budget": "call" + "callout": "f" }, "energy": { "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" @@ -91,8 +90,8 @@ "alg": "ed25519", "canon": "rfc8785", "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", - "digest": "bd9c3117100fc749a7600c429496074a5104c9c98451b859236a3a2c126b93bb", - "signature": "49c9586b6875a005bf312823a8608205fa0d093da7eaa727596bd504146d7c8c3c362b00ac7798af68cc81f6c42707c057702e45110700ed5ea039cbb586a309" + "digest": "53a25ec1ea2c311a18d13793c7cba18d1c53047f749d8dab06d90a0ba3dbe053", + "signature": "350f02d3b50e52ec29bd2b1bba2c4653be37f8153ee7f3f2689990903ee1e7119d7ec3de1eb18304aaacaacd10eed8772fb64f2d9804adccab3a6a60cf207801" } } } diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-instance-after-call.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-instance-after-call.json new file mode 100644 index 000000000..17f20cc54 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-first-instance-after-call.json @@ -0,0 +1,96 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3", + "bytes": 50, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "shot" + } + }, + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 0, + "seed": 1, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 2, + "instances": 1, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 1, + "max": 1 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3" + } + ], + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "init", + "module": "m", + "fuel": 1 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "ae7a6dc8dcb90b1060a50adb3bcf335906d9984e08ca0cd36d69bbab150213bd", + "signature": "1ae288c8b53b9552f903443c9a2f650cba16dd2ab191b0c024e10276d6bd49e963a40d7b640179c2a0c3fc850df773fac2f52852d0174beca69f8d1d1b02bd0b" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-frames-past-envelope.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-frames-past-envelope.json new file mode 100644 index 000000000..6ca010f4c --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-frames-past-envelope.json @@ -0,0 +1,96 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "2bd6df6f77cff04a53312cc69ee9024ae6e445af9dd3a7d410cc5c8a18c0c4d0", + "bytes": 105, + "name": "m", + "priors": [ + { + "blake3": "024f67c0425a3dc02fbaf58cb93de5132e3d75c519faa0bada21491d88c97057", + "bytes": 134217728, + "name": "weights" + } + ] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "shot" + } + }, + "envelope_blake3": "6da2c86da1b3d288df324fa7902e4cb10c9c5b21160921907299402466c5d50c" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 0, + "seed": 1, + "counts": {}, + "callouts": 0, + "fuel": { + "used": 8, + "instances": 8, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": {} + }, + "modules": [ + { + "name": "m", + "blake3": "2bd6df6f77cff04a53312cc69ee9024ae6e445af9dd3a7d410cc5c8a18c0c4d0" + } + ], + "transcript_blake3": "3db8f511346d060c257940c086300666c8eb1b27c6083d36066b8d8f38e6408c", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "prior", + "module": "m", + "fuel": 8 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "5dd5e46465d0286cb9acc026e233131fb1d04963f5d9e4169c31f2d8a1e41f44", + "signature": "00636291efa15d23752e5057f5bb8e083867cdc4ae1c229f092eb3ee5636ba9c71d12c582f195f7723dbf3d00f8e1993a8bedb5c4d3964c64c0b27a1c8c74907" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-fault-under-limit.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-fault-under-limit.json new file mode 100644 index 000000000..beaa4b91a --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-fuel-fault-under-limit.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1", + "bytes": 57, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 400, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 9, + "shots": 32, + "state": "shot" + } + }, + "envelope_blake3": "22d84ca179e6f5c69a4a2eb93e9bdc545b021add204c96cb1466c306119b0b35" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 32, + "shots_run": 0, + "seed": 9, + "counts": {}, + "callouts": 1, + "fuel": { + "used": 10, + "instances": 0, + "budget": { + "per_call": 400, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 10, + "max": 10 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "a1341d4aec22430d9f26a6a9044e98a92a1e67d97743fa64eee9720c835eb9b1" + } + ], + "transcript_blake3": "47e0790eb680fb9b05637b791d60d2614cf74adea1114bbceb36ee237f9839f7", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 0, + "phase": "call", + "module": "m", + "fuel": 10, + "callout": "f", + "budget": "call" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "b72c6b8860cc79ce104c2489d000729762d6c9f3cee5148dd8273643f6cc97cb", + "signature": "b6fd7f4115465f57855054e8fe3d92a050394f1b0585a4ea7813dd452a028ee374854fd4823ada97a2131c54bc4d2b02ff24c6783940dfcf201b2e698c967300" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-job-state-later-instance.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-job-state-later-instance.json new file mode 100644 index 000000000..8b3b2d2a0 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-job-state-later-instance.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3", + "bytes": 50, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "job" + } + }, + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 2, + "seed": 1, + "counts": { + "0": 2 + }, + "callouts": 2, + "fuel": { + "used": 3, + "instances": 1, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 2, + "fuel": 2, + "max": 1 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3" + } + ], + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "unreachable", + "shot": 2, + "phase": "init", + "module": "m", + "fuel": 1 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "78984419848a7a5013c37c8248d263680434db308a42a4bccb8961a20d72b7a5", + "signature": "512d2164ebff9225a6004056c738a042d8a7cf00bef1da03bf8f21ba25ff5e5ad6205dd174a0a68de4c40ff968aafec928a4c37e5cd015f9253bd5a0757d2a01" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-lifecycle-fuel-past-instances.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-lifecycle-fuel-past-instances.json new file mode 100644 index 000000000..f2f1d06df --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-lifecycle-fuel-past-instances.json @@ -0,0 +1,98 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3", + "bytes": 50, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "shot" + } + }, + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 1, + "seed": 1, + "counts": { + "0": 1 + }, + "callouts": 1, + "fuel": { + "used": 2, + "instances": 1, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 1, + "fuel": 1, + "max": 1 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3" + } + ], + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "unreachable", + "shot": 1, + "phase": "init", + "module": "m", + "fuel": 2 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "cfc4ab8a2ac1e68009697a596ef65943ab215d888a1ced72b16bce0770b4d88d", + "signature": "94de72874b843958e61c1f79d4274feca3536b6d8cb504be1c22993312d1df540e7bec8a0c919e3c67e34da03c51ffd5c858600a180e68f9d3f0cd6c96248b04" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-module-empty.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-module-empty.json new file mode 100644 index 000000000..65acb8657 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-module-empty.json @@ -0,0 +1,110 @@ +{ + "status": "done", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "decode64", + "extern": "decode64", + "module": 0 + } + ], + "modules": [ + { + "blake3": "005631822744a3b2271dfc6c4a7dd9412bb4eca91740cc06f1cda2d5cec11962", + "bytes": 0, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "cd573b96d3b69c2044b06d60be8913abb651fdd09a6c71af5c364a0f511a17f7", + "signature": "3c91f0be4f76a74b87ae1e4140822462c263e63b9a9b84096731f3260057fe22c76afcba184c53420c3ba00ffa30370ff2b00fdf5949e378adb303110331060a" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-prior-fault-no-prior.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-prior-fault-no-prior.json new file mode 100644 index 000000000..7e890a286 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-prior-fault-no-prior.json @@ -0,0 +1,90 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3", + "bytes": 50, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 17179869184 + }, + "method": "stabilizer", + "seed": 1, + "shots": 4, + "state": "shot" + } + }, + "envelope_blake3": "d3cc9387b18e2f2cd1e49379249884bf0cdc58399e3fe031933e4e17a7a538f9" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 4, + "shots_run": 0, + "seed": 1, + "counts": {}, + "callouts": 0, + "fuel": { + "used": 1, + "instances": 1, + "budget": { + "per_call": 10000000, + "per_job": 17179869184, + "per_instance": 100000000 + }, + "callouts": {} + }, + "modules": [ + { + "name": "m", + "blake3": "f3f2aabf3edb17dd4e18532dcf6291bb6b806371b85e6c1e27af3da77dab93b3" + } + ], + "transcript_blake3": "1f9cfb398c872903d55e1962f50421060c42968e524e914e04bb0b971e4d0881", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "unreachable", + "shot": 0, + "phase": "prior", + "module": "m", + "fuel": 1 + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "f7ccd98fc1b31bc3ba2a488bb2b96a4ac8387ad99d9f51408d78c5dffe8754ed", + "signature": "ea2cddbe43f18b519487586c21bae5ff3ecd5449d2dd699946f95b0c0ee7845662b799cfc52c78be35e625d502afca357a904ae5b2ee1888af28e832764ae70b" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-priors-unpassed.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-priors-unpassed.json new file mode 100644 index 000000000..d437c353f --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-priors-unpassed.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": 591195, + "instances": 2847, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "5e1db5f93650ca8e15d1ef564577151c9c2b0d01a799f13834ff3923159e44d1", + "signature": "9c24b0b3092588a2a89218c0f16d294ae0cce0c3f25f22c28dd487a2be5cdd89437b6bfd3563a619f1f87279d3c9261ea986c61e2249e64a6b21989732385a05" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-program-empty.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-program-empty.json new file mode 100644 index 000000000..024aca90c --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-program-empty.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": 0, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "1d897b197a92ccb06456d13ec048db95cb4ef3ff7baf7d8b6af3b9a081998673", + "signature": "5978b536b336e3537a11f17ef7e969a50be122e74d7e5d4478840fd4742f821bbc64b491cfb3acb67b262f92629b4d6d7668ed5edcfe9d4d3f982825455bd804" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-request-member-added.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-request-member-added.json new file mode 100644 index 000000000..c899e3b4f --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-request-member-added.json @@ -0,0 +1,111 @@ +{ + "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", + "x": 1 + }, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "d70fdae9305fcc22843ecb996e95e0f2f55d2012575a5f3018885a5db1d0bdf0", + "signature": "ff922f40dc484319c3c7565f737bcd821486db77fcecceeb8a6062f18b0eeff76b5f935cc27b65c354c3a07e9ee63bb86d4e764c17de7447e5079bd5df882706" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-seed.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-seed.json new file mode 100644 index 000000000..2b1f99b22 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-result-seed.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": 1235, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "f7d44a5e34e72ecfff5410ca0aadc927fbc4fca15eca88db2c4b4d8c845419b3", + "signature": "4309cc7a5ef605a8363d663e5bcf669ee7735f29f7f81f3affa6a19a99732cf85bdb9b0b43b1f07cac0110174b47598dd482c1e0048465dd3b16be57fd8dd906" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-results-past-bits.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-results-past-bits.json new file mode 100644 index 000000000..1b9f13080 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-results-past-bits.json @@ -0,0 +1,100 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43", + "bytes": 68, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 50 + }, + "method": "stabilizer", + "seed": 1, + "shots": 1048576, + "state": "shot" + } + }, + "envelope_blake3": "0a19542a6a9178984a93d9863a1d5e9371e00b86c6e9143e04b5f0fb5b0be177" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 1048576, + "shots_run": 2, + "seed": 1, + "counts": { + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": 2 + }, + "callouts": 3, + "fuel": { + "used": 50, + "instances": 0, + "budget": { + "per_call": 10000000, + "per_job": 50, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 3, + "fuel": 50, + "max": 23 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43" + } + ], + "transcript_blake3": "3772395e9bb2f9fba3b1a7ef4f5def3e5f10f76a9ba517d16056e5fd7cb7d413", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 2, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f", + "budget": "run" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "5c0940231cdea3e717c8f4a21dc9b8cb6c5e7a2a05a95daf14ead02e4dba8e21", + "signature": "1ecb47df023cab2f4085bb6c6d3a81b8c62f89a94e33496f82fca9065a3cf3e8fd43610750fd3a799c9492ffe30a8c5c72e63f6b7f18d82c064520a8b2ae7307" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-run-budget-not-spent.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-run-budget-not-spent.json new file mode 100644 index 000000000..38032e7b7 --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-run-budget-not-spent.json @@ -0,0 +1,101 @@ +{ + "status": "fault", + "method": "stabilizer", + "request": { + "wasmq": { + "callouts": [ + { + "export": "f", + "extern": "f", + "module": 0 + } + ], + "modules": [ + { + "blake3": "9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43", + "bytes": 68, + "name": "m", + "priors": [] + } + ], + "profile": "wai.quantum.wasmq/1", + "program": { + "blake3": "3e7ed8c589abdebfae8431e20bb8b2f7f1392cfb3ad101fb50002b4a11a18319", + "bytes": 128, + "inputs": {} + }, + "run": { + "fuel": { + "per_call": 10000000, + "per_instance": 100000000, + "per_job": 60 + }, + "method": "stabilizer", + "seed": 1, + "shots": 8, + "state": "shot" + } + }, + "envelope_blake3": "0a19542a6a9178984a93d9863a1d5e9371e00b86c6e9143e04b5f0fb5b0be177" + }, + "result": { + "format": "wai.quantum.wasmq.result/1", + "profile": "wai.quantum.wasmq/1", + "status": "fault", + "method": "stabilizer", + "qubits": 8, + "shots": 8, + "shots_run": 2, + "seed": 1, + "counts": { + "0101101101011011": 1, + "1011111010111110": 1 + }, + "callouts": 3, + "fuel": { + "used": 50, + "instances": 0, + "budget": { + "per_call": 10000000, + "per_job": 60, + "per_instance": 100000000 + }, + "callouts": { + "f": { + "calls": 3, + "fuel": 50, + "max": 23 + } + } + }, + "modules": [ + { + "name": "m", + "blake3": "9de978f8d33833a7abb3ab51af439869309b3d7a6d11d0066650c22c26542d43" + } + ], + "transcript_blake3": "3772395e9bb2f9fba3b1a7ef4f5def3e5f10f76a9ba517d16056e5fd7cb7d413", + "accuracy": { + "class": "exact" + }, + "fault": { + "reason": "fuel_exhausted", + "shot": 2, + "phase": "call", + "module": "m", + "fuel": 4, + "callout": "f", + "budget": "run" + }, + "energy": { + "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" + }, + "receipt": { + "alg": "ed25519", + "canon": "rfc8785", + "key": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "39a21b49ebb0c7a9e22e2594c4b4afdb06edde9f68de01c275b65075984db36a", + "signature": "8b70291f63f1fcd478991416cf2707046db92bac88b3658c1fc240eef45a1001d9549f23bd58211017455e83eb6835605a3148c16bd29997a509637265453a01" + } + } +} diff --git a/wai/quantum-conformance/receipts/jobs/crafted-wasmq-work-past-bound.json b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-work-past-bound.json new file mode 100644 index 000000000..b3d15abca --- /dev/null +++ b/wai/quantum-conformance/receipts/jobs/crafted-wasmq-work-past-bound.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": 549755813888 + }, + "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": 549755813888, + "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": "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "digest": "eed9dde93614e853b2944351ff11bb3f0e5707493f82fcc901f8db7ced11ae5f", + "signature": "25dbf3e5c4672c479054261b06bb76028fa023712eef2618e859e518e8026da763e9c349049a4b4945ffa92fcbc616dec0b97d2c8effd0f29c1553e6c7617105" + } + } +} diff --git a/wai/quantum-conformance/receipts/manifest.json b/wai/quantum-conformance/receipts/manifest.json index ecc929c39..6bfea3286 100644 --- a/wai/quantum-conformance/receipts/manifest.json +++ b/wai/quantum-conformance/receipts/manifest.json @@ -1309,7 +1309,175 @@ "wasmq_form" ], "made_by": "crafted", - "what": "a call that stopped after 400 fuel, where the most one call of it used is 200 (rule 18)" + "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 two calls take (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 (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 (rule 27)" + }, + { + "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", diff --git a/wai/quantum-conformance/tools/receipt_differential.py b/wai/quantum-conformance/tools/receipt_differential.py index dbf38c6a8..1e5c6509f 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): @@ -229,13 +331,19 @@ def rule_cases(corpus, seal): def load(name): with open(os.path.join(corpus, "jobs", name), encoding="utf-8") as f: return json.load(f) - run, fault, init, job = (load("cli-wasmq-%s.json" % n) for n in ("run", "fault", "fault-init", "state-job")) + 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): + def case(rule, what, base, edit, valid, respell=None): b = copy.deepcopy(base) edit(b) - out.append(("rule %s, %s" % (rule, what), seal(b), [] if valid else ["wasmq_form"])) + sealed = seal(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"] @@ -277,13 +385,52 @@ def rule_cases(corpus, seal): (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). + return sum(p["bytes"] + 2 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"]) + 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 own_fuel(delta): def f(b): e = calls(b) @@ -305,26 +452,42 @@ def rule_cases(corpus, seal): 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 2^27 bytes", run, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27), True) - case(6, "a prior of 2^27 + 1 bytes", run, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 + 1), 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) # 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"): - case(8, "the budget %s at 2^39" % name, run, budget(name, 2 ** 39), True) - case(8, "the budget %s at 2^39 + 1" % name, run, budget(name, 2 ** 39 + 1), False) + # 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) @@ -337,6 +500,8 @@ def rule_cases(corpus, seal): 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, "0 qubits", run, at(R + ("qubits",), 0), True) + 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) @@ -348,7 +513,7 @@ def rule_cases(corpus, seal): case(14, "a call's largest equal to its fuel", run, 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(14, "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) + 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 @@ -362,18 +527,149 @@ def rule_cases(corpus, seal): 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(18, "a call's fuel fault of the run's budget", fault, at(R + ("fault", "budget"), "run"), True) + 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={}, used=0), b["result"]["fault"].update(fuel=0)), 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=400, max=400)), 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=400, max=200)), 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=2, fuel=2), True) + 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), 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 frames + case(31, "frames one short of 2^27", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 - 1 - (frames(b) - contract(b)["modules"][0]["priors"][0]["bytes"])), True) + case(31, "frames of 2^27", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 - (frames(b) - contract(b)["modules"][0]["priors"][0]["bytes"])), 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 @@ -384,7 +680,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 @@ -399,22 +701,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. @@ -430,8 +758,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 @@ -455,6 +781,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 @@ -472,9 +799,6 @@ 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()))) - - problems += rules_stage(ref, a.plant, seal) - return finish(a, problems) @@ -485,16 +809,54 @@ def rules_stage(ref, plant, seal): 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(compact(b) for _, b, _ in cases) + "\n") + 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", [])))) - rules = len({w.split(",")[0] for w, _, _ in cases}) - print("rules: %d cases at the bounds of %d rules of a wasmQ job's form, sealed again; %d as written" % (len(cases), rules, len(cases) - len({p.split(":")[0] for p in problems}))) + 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 diff --git a/wai/quantum-conformance/tools/receipt_verify.py b/wai/quantum-conformance/tools/receipt_verify.py index 869f3fd8b..821682a22 100644 --- a/wai/quantum-conformance/tools/receipt_verify.py +++ b/wai/quantum-conformance/tools/receipt_verify.py @@ -11,8 +11,15 @@ 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 @@ -57,7 +64,9 @@ 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. Where -quantum-wasmq §6.5 leaves one, the reading taken is marked SPEC-GAP [W1] to [W14]. +quantum-wasmq §6.5 leaves one, the reading taken is marked SPEC-GAP [Wn]: [W2], [W5], [W9] +to [W14], [W17] and [W18]. [W1], [W3], [W4], [W6], [W7], [W8], [W15] and [W16] are gone: +the text now settles them. """ import hashlib @@ -1056,7 +1065,9 @@ def provider_statement(result): # 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. +# made; the statement of outcome, problems and notes) by a reader who had not seen it; +# and again for rules 20 to 31 (the ties between members) and rule 6's counts, 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 @@ -1077,7 +1088,7 @@ WASMQ_FAULT_REASONS = ( "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 = 19 +WASMQ_RULES = 31 WASMQ_PROBLEMS_SHOWN = 64 # §6.5: the first 64 problems, then how many more _ABSENT = object() # a member not present: equal to nothing @@ -1150,8 +1161,7 @@ def wasmq_broken(request, result): program, run = _obj(program_given), _obj(run_given) budgets_given = _get(run, "fuel") budgets = _obj(budgets_given) - # SPEC-GAP: [W1] "run's per_call", "per_job", "per_instance": run has no such member; - # read as its fuel's, the three budgets rule 8 names. + # §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) @@ -1188,14 +1198,16 @@ def wasmq_broken(request, result): 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. + # 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_in(p["bytes"], 0, 2 ** 27)) + 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_in(m["bytes"], 1, 2 ** 19) + 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] @@ -1217,18 +1229,18 @@ def wasmq_broken(request, result): 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 at most 2^39. + # 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 all(_count_in(b, 0, 2 ** 39) for b in (per_call, per_job, per_instance))) + 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's canon is rfc8785. - # SPEC-GAP: [W3] the receipt is not sealed, though §6.5 says only what is sealed - # decides; rule 10 names the receipt, and is read as written: a receipt that is not - # an object, or of the first form, breaks it. + # 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). @@ -1248,8 +1260,7 @@ def wasmq_broken(request, result): # 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 at most calls × max. - # SPEC-GAP: [W4] "at most fuel": the member's own fuel, not F. + # 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) @@ -1259,8 +1270,7 @@ def wasmq_broken(request, result): 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"]) - and _n(e["fuel"]) <= _n(e["calls"]) * _n(e["max"])) + 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())) @@ -1287,9 +1297,8 @@ def wasmq_broken(request, result): 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, otherwise shots_run, and 1 more when status is fault. - # SPEC-GAP: [W6] "and 1 more when r's status is fault" is read inside "otherwise": - # with state job one instance serves the run, a fault or not. + # 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 @@ -1301,14 +1310,13 @@ def wasmq_broken(request, result): 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, used, modules, externs, - f_callouts, per_call, per_instance)) + 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, all of one length, each at least - # 1, adding up to shots_run. - # SPEC-GAP: [W7] an empty outcome holds only 0 and 1 (none), as §8.4's counts reads one. + # 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) @@ -1316,10 +1324,148 @@ def wasmq_broken(request, result): 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 31 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: its fuel less s × the fault's fuel (s 1 when the fault + # is a call's fault whose callout is that member's name, else 0; the fault's fuel 0 when + # it is no count), 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); calls, fuel and max counts. + # "(0 when that is not a count)" is read of the fault's fuel: the member's own fuel is + # held to be a count by the rule's last sentence. + stopped_fuel = _or_zero(fault_fuel) + + def returned_holds(name, e): + stopped = 1 if call_fault and _same(_get(fault, "callout"), name) else 0 + e_calls, e_fuel, e_max = _get(e, "calls"), _get(e, "fuel"), _get(e, "max") + if not (_count(e_calls) and _count(e_fuel) and _count(e_max)): + return False + returned = _n(e_fuel) - stopped * stopped_fuel + returning = max(_n(e_calls) - stopped, 0) + return returned >= 0 and returning <= returned <= returning * _n(e_max) + + rule(20, all(returned_holds(name, e) for name, e in f_callouts.items())) + + # 21. F's instances is a count of at least W × P: P, over every prior of every one of C's + # modules, 2 and the prior's bytes (0 when no count); 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. + passing = sum(2 + _or_zero(_get(p, "bytes")) for m in modules for p in _arr(_get(m, "priors"))) + made_whole = _or_zero(shots_run) + (1 if call_fault else 0) + if state_job: + made_whole = min(made_whole, 1) + rule(21, _count(instances) and _n(instances) >= made_whole * passing) + + # 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 of at most F's instances (a count). + rule(25, not lifecycle_fault + or (_count(fault_fuel) and _count(instances) and _n(fault_fuel) <= _n(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 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). + # SPEC-GAP: [W17] "the length of the longest": the text gives no unit; a name's length + # is read as its characters (Unicode code points), one an outcome's bit. It differs from + # its UTF-8 bytes or UTF-16 units only for a name that is not of 0 and 1, which rule + # 19 refuses. + 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 + 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); 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) + 8 * _or_zero(per_job)) + rule(30, work <= 2 ** 42) + + # 31. The program's bytes, each module's and each prior's, added (each 0 when no count), + # are less than 2^27. + frames = (_or_zero(_get(program, "bytes")) + sum(code) + + sum(_or_zero(_get(p, "bytes")) for m in modules for p in _arr(_get(m, "priors")))) + rule(31, frames < 2 ** 27) + return sorted(broken) -def _fault_holds(fault, shots, shots_run, used, modules, externs, f_callouts, per_call, per_instance): +def _fault_holds(fault, shots, shots_run, modules, externs, f_callouts, per_instance): """§6.5 rule 18, for a result whose status is fault. `f_callouts` is F's callouts, read as an empty object when it is not one (rule 14 then fails, and so does this clause: no member is named in it).""" @@ -1328,15 +1474,16 @@ def _fault_holds(fault, shots, shots_run, used, modules, externs, f_callouts, pe return False reason, phase = fault["reason"], fault["phase"] in_call = _text_is(phase, "call") - # SPEC-GAP: [W8] a phase not among the four is "otherwise": per_instance. - phase_budget = per_call if in_call else per_instance return (_text_is(reason, *WASMQ_FAULT_REASONS) and _count(shots_run) and _count(shots) and _n(shots_run) < _n(shots) and _count(fault["shot"]) and _n(fault["shot"]) == _n(shots_run) and _text_is(phase, *WASMQ_PHASES) and any(_same(fault["module"], _get(m, "name")) for m in modules) - and _count(fault["fuel"]) and _count(used) and _n(fault["fuel"]) <= _n(used) - and _n(fault["fuel"]) <= _or_zero(phase_budget) + # fuel a count, and in a phase other than call (one not among the four + # included) at most per_instance: a call's is held by its callout's max, + # below, and rule 14; rules 20 and 25 hold either within F's used + and _count(fault["fuel"]) + and (in_call or _n(fault["fuel"]) <= _or_zero(per_instance)) # a callout exactly in a call (present, whatever its value), one of C's # externs 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 @@ -1353,13 +1500,12 @@ def _stopped_call_made(callout, fuel, f_callouts): """§6.5 rule 18: the fault's callout names a member of F's callouts whose max is at least the fault's fuel (a count, checked before this is asked).""" # SPEC-GAP: [W9] "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 second "that" read as - # the extern (a callout names a module and an export, not a member of F), so the - # fault's callout is one of C's externs, and F's callouts has a member of that very - # name whose max is a count at least the fault's fuel. "At least" compares counts - # as "at most" does: a max that is no count fails it. The fault's module is not - # held to the module of that callout: the rule says only that it is the name of one - # of C's modules, and names are for people (§2.2), so none is read here. + # F's callouts whose max is at least the fault's fuel": a callout names a member of + # F's callouts by its extern (F's callouts are keyed by extern, §6.4), so the fault's + # callout is one of C's externs, and F's callouts has a member of that very name whose + # max is a count at least the fault's fuel. "At least" compares counts as "at most" + # and "equals" do (§6.5 names those two only): a max that is no count fails it. The + # fault's module against that callout's module is rule 22's, not this rule's. entry = _get(f_callouts, callout) if isinstance(callout, str) else _ABSENT top = _get(entry, "max") return _count(top) and _n(top) >= _n(fuel) @@ -1402,6 +1548,22 @@ WASMQ_RULE_WORDS = { 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, or outside 1 to max for each call that returned", + 21: "fuel's instances are no count of at least W x P, the fuel of passing each prior to " + "each shot's instances", + 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 of at most 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, per_job) is past 2^42", + 31: "the program's, modules' and priors' bytes add up to 2^27 or more", } @@ -1423,7 +1585,7 @@ 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 19, so a job never reaches it; the self-test calls it alone. + # rule gives at most 31, so a job never reaches it; the self-test calls it alone. items = list(items) if len(items) <= shown: return items @@ -1757,7 +1919,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 @@ -1766,6 +1929,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] @@ -2025,6 +2190,718 @@ 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 31 ask: + the run's instances, 3000, are at least W × P = 32 × (2 + 87) = 2848 (rule 21); each + member of its callouts has fuel from calls to calls × max (rule 20). The fault's run has + W = 1 (state job), P = 89 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); decode64 is the extern of callout 0, whose module 0 is the decoder (rule 22). + Either's work (rule 30) is some 2^37, and its frames (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": 87}]}, + {"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 + + +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.""" + 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) + 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, with no callout and no budget. + seed_trap = {"fault__reason": "unreachable", "fault__phase": "seed", "fault__callout": _DELETE, + "fault__budget": _DELETE} + 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). + first_shot = {"shots_run": 0, "counts": {}, "callouts": 0, "fuel__used": 50, "fuel__instances": 50, + "fuel__callouts": {}, + "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} + 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, all but 8 × per_job comes to 5 440 030 336 (state + # shot, 17 qubits), 5 440 033 472 (33 qubits, w = 2) and 5 438 236 242 (state job, I = 1). + work_at = 549075810096 # (2^42 − 5 440 030 336) / 8: the work at 2^42 + work_at_33 = 549075809704 # (2^42 − 5 440 033 472) / 8 + work_job = 549076034357 # ⌊(2^42 − 5 438 236 242) / 8⌋: the work 2^42 − 6 + # Rule 31: the frames are 1104 + 39442 + 2048 and the prior's bytes; state job, so that + # W = 1 and the instances need hold one passing of the prior (rule 21). + frames_but_prior = 1104 + 39442 + 2048 + big_prior = {"fuel__instances": 2 ** 27 - frames_but_prior + 2, + "fuel__used": 2 ** 27 - frames_but_prior + 2 + 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}), [], []), + ("wasmq: qubits 0", _wasmq_job(result={"qubits": 0}), [], []), + ("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, and rule 21 counts them in the + # instances' fuel (W × P = 32 × (2 + 2^27 + 1), past the instances' 3000). + ("wasmq rules 21 and 31: 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]), + # 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]), + ("wasmq rule 14: max past its fuel", _wasmq_job(result={"fuel__callouts__plan__max": 120}), form, [14]), + # 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 31. Taken, rule 20 (s = 1: + # 401 - 400 below 2 calls) and rule 28 (budget run, used 801 not per_job) would fail. + ("wasmq rule 18: status done beside a fault, no fault of rules 20 to 31", _wasmq_job(fault=True, result={ + "status": "done", "fault__budget": "run", "fuel__callouts__decode64__fuel": 401, "fuel__used": 801}), + 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 31): in state job at + # shot 2, rule 26 fails too. + ("wasmq rule 18: another phase", _wasmq_job(fault=True, result={"fault__phase": "run"}), 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 (P = 89). + ("wasmq: instances at W × P, state shot (W = 32)", _wasmq_job(result={ + "fuel__instances": 2848, "fuel__used": 9348}), [], []), + ("wasmq rule 21: instances below W × P, state shot (W = 32)", _wasmq_job(result={ + "fuel__instances": 2847, "fuel__used": 9347}), 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": 267, "fuel__used": 1267}), [], []), + ("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": 266, "fuel__used": 1266}), form, [21]), + ("wasmq: instances at W × P, a lifecycle fault in state shot (W = 2)", _wasmq_job( + fault=True, request=shot_state, result=dict( + seed_trap, fault__fuel=100, fuel__instances=178, fuel__used=1178)), [], []), + ("wasmq rule 21: instances below W × P, a lifecycle fault in state shot (W = 2)", _wasmq_job( + fault=True, request=shot_state, result=dict( + seed_trap, fault__fuel=100, fuel__instances=177, fuel__used=1177)), form, [21]), + ("wasmq: instances at W × P, state job (W = 1)", _wasmq_job(fault=True, result={ + "fuel__instances": 89, "fuel__used": 1089}), [], []), + ("wasmq rule 21: instances below W × P, state job (W = 1)", _wasmq_job(fault=True, result={ + "fuel__instances": 88, "fuel__used": 1088}), form, [21]), + + # 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 is part of the instances' (400 in "a trap in seed"). + ("wasmq rule 25: a trap in seed past the instances' fuel", _wasmq_job( + fault=True, request=shot_state, result=dict(seed_trap, fault__fuel=401)), 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). 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}), 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]), + ("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]), + ("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]), + + # 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]), + # "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. + ("wasmq: the work at 2^42", _wasmq_job(request={"wasmq__run__fuel__per_job": work_at}, + result={"fuel__budget__per_job": work_at}), [], []), + ("wasmq rule 30: the work past 2^42", _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, 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 past 2^42, 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 - 6, 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 + 2, 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 frames an envelope of at most 128 MiB holds. + ("wasmq: the frames at 2^27 - 1 bytes", _wasmq_job(request=dict( + job_state, wasmq__modules__0__priors__0__bytes=2 ** 27 - 1 - frames_but_prior), result=big_prior), + [], []), + ("wasmq rule 31: the frames at 2^27 bytes", _wasmq_job(request=dict( + job_state, wasmq__modules__0__priors__0__bytes=2 ** 27 - frames_but_prior), result=big_prior), + form, [31]), + + # 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__max": 120}), 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 = [] @@ -2471,391 +3348,52 @@ 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: a wasmQ job made and sealed here, run to its end or stopped by a - # fault, and each rule broken alone (the job sealed again, so that its seal holds). - # Rule 1 cannot be broken alone: 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, so it is broken beside one. - def wasmq_parts(fault): - 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": 87}]}, - {"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": 6900, "instances": 400, "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, callouts={"decode64": {"calls": 3, "fuel": 1000, "max": 400}}) - return asked, made - - def wasmq_job(fault=False, request=None, result=None, envelope='"status":"done","method":"stabilizer",', - **seal_options): - """A wasmQ job, each change a path (members joined by "__") and its value.""" - 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(value) - else: - node[last] = value - return _seal(seed, _dump(asked), _dump(made), envelope=envelope, **seal_options) - + # 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 changed_after(raw, change): - job = json.loads(raw.decode()) - change(job) - return json.dumps(job).encode() - - plan_fuel = {"calls": 2, "fuel": 100, "max": 60} - wasmq_cases = ( - # (name, job, the reasons, the rules of §6.5 it breaks, None for no wasmQ job) - ("wasmq: a run", wasmq_job(), [], []), - ("wasmq: a run stopped by a fault", wasmq_job(fault=True, envelope='"status":"fault","method":"stabilizer",'), - [], []), - ("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}), [], []), - ("wasmq: qubits 0", wasmq_job(result={"qubits": 0}), [], []), - ("wasmq: qubits 2^15", wasmq_job(result={"qubits": 2 ** 15}), [], []), - ("wasmq: instances and used moved together", wasmq_job(result={"fuel__instances": 401, "fuel__used": 6901}), - [], []), - ("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, result={ - "fault__phase": "init", "fault__budget": "instance", "fault__callout": _DELETE}), [], []), - ("wasmq: a fault of the run's budget in a call", wasmq_job(fault=True, result={"fault__budget": "run"}), - [], []), - ("wasmq: a trap in seed", wasmq_job(fault=True, result={ - "fault__reason": "unreachable", "fault__phase": "seed", "fault__callout": _DELETE, - "fault__budget": _DELETE}), [], []), - ("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={ - "fault__fuel": 150, "fault__budget": "run"}), [], []), - ("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. - ("wasmq rule 1: an input of 2^53", wasmq_job(request={"wasmq__program__inputs__rounds": 2 ** 53}), - ["wasmq_form"], [1, 5]), - ("wasmq rule 1: an input of -2^53", wasmq_job(request={"wasmq__program__inputs__offset": -2 ** 53}), - ["wasmq_form"], [1, 5]), - ("wasmq rule 1: a seed of 2^53", wasmq_job(request={"wasmq__run__seed": 2 ** 53}, result={"seed": 2 ** 53}), - ["wasmq_form"], [1, 8]), - # Rules 2 to 19, each alone. - ("wasmq rule 2: envelope_blake3 in upper case", wasmq_job(request={"envelope_blake3": "E5" * 32}), - ["wasmq_form"], [2]), - ("wasmq rule 2: a member more", wasmq_job(request={"note": "x"}), ["wasmq_form"], [2]), - ("wasmq rule 2: envelope_blake3 missing", wasmq_job(request={"envelope_blake3": _DELETE}), - ["wasmq_form"], [2]), - ("wasmq rule 3: the program a member more", wasmq_job(request={"wasmq__program__note": "x"}), - ["wasmq_form"], [3]), - ("wasmq rule 3: the contract a member more", wasmq_job(request={"wasmq__note": "x"}), ["wasmq_form"], [3]), - ("wasmq rule 3: run a member more", wasmq_job(request={"wasmq__run__note": "x"}), ["wasmq_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}), ["wasmq_form"], [3]), - ("wasmq rule 4: another profile", wasmq_job(request={"wasmq__profile": "wai.quantum.wasmq/2"}), - ["wasmq_form"], [4]), - ("wasmq rule 5: no bytes", wasmq_job(request={"wasmq__program__bytes": 0}), ["wasmq_form"], [5]), - ("wasmq rule 5: past 2^20 bytes", wasmq_job(request={"wasmq__program__bytes": 2 ** 20 + 1}), - ["wasmq_form"], [5]), - ("wasmq rule 5: an input not whole", wasmq_job(request={"wasmq__program__inputs__rounds": 1.5}), - ["wasmq_form"], [5]), - ("wasmq rule 5: inputs an array", wasmq_job(request={"wasmq__program__inputs": []}), ["wasmq_form"], [5]), - ("wasmq rule 5: the program's digest in upper case", wasmq_job(request={ - "wasmq__program__blake3": "A1" * 32}), ["wasmq_form"], [5]), - ("wasmq rule 6: a module of no bytes", wasmq_job(request={"wasmq__modules__0__bytes": 0}), - ["wasmq_form"], [6]), - ("wasmq rule 6: the modules past 2^19 bytes together", wasmq_job(request={ - "wasmq__modules__0__bytes": 2 ** 19}), ["wasmq_form"], [6]), - ("wasmq rule 6: a prior past 2^27 bytes", wasmq_job(request={ - "wasmq__modules__0__priors__0__bytes": 2 ** 27 + 1}), ["wasmq_form"], [6]), - ("wasmq rule 6: a prior a member more", wasmq_job(request={"wasmq__modules__0__priors__0__note": "x"}), - ["wasmq_form"], [6]), - ("wasmq rule 6: priors an object", wasmq_job(request={"wasmq__modules__0__priors": {}}), - ["wasmq_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}), ["wasmq_form"], [6]), - ("wasmq rule 7: a module no callout names", wasmq_job(request={"wasmq__callouts__1__module": 0}), - ["wasmq_form"], [7]), - ("wasmq rule 7: a module index past the modules", wasmq_job(request={"wasmq__callouts__1__module": 2}), - ["wasmq_form"], [7]), - ("wasmq rule 7: an extern twice", wasmq_job(request={"wasmq__callouts__2": { - "extern": "decode64", "module": 1, "export": "decode"}}), ["wasmq_form"], [7]), - ("wasmq rule 7: a module index not whole", wasmq_job(request={"wasmq__callouts__1__module": 1.5}), - ["wasmq_form"], [7]), - ("wasmq rule 7: an export not a string", wasmq_job(request={"wasmq__callouts__0__export": 7}), - ["wasmq_form"], [7]), - ("wasmq rule 8: another state", wasmq_job(request={"wasmq__run__state": "batch"}), ["wasmq_form"], [8]), - ("wasmq rule 8: another method, alike in the result", wasmq_job( - request={"wasmq__run__method": "statevector"}, result={"method": "statevector"}), ["wasmq_form"], [8]), - ("wasmq rule 8: a seed below 0, alike in the result", wasmq_job( - request={"wasmq__run__seed": -1}, result={"seed": -1}), ["wasmq_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}), - ["wasmq_form"], [8]), - ("wasmq rule 9: a member more", wasmq_job(result={"note": "x"}), ["wasmq_form"], [9]), - ("wasmq rule 10: sealed in the first form", wasmq_job(form="first"), ["wasmq_form"], [10]), - ("wasmq rule 11: another profile", wasmq_job(result={"profile": "wai.quantum.wasmq/2"}), - ["wasmq_form"], [11]), - ("wasmq rule 11: accuracy a member more", wasmq_job(result={"accuracy__note": "x"}), ["wasmq_form"], [11]), - ("wasmq rule 11: another energy", wasmq_job(result={"energy__unmetered": "x"}), ["wasmq_form"], [11]), - ("wasmq rule 11: another seed", wasmq_job(result={"seed": 1235}), ["wasmq_form"], [11]), - ("wasmq rule 11: another method", wasmq_job(result={"method": "statevector"}), ["wasmq_form"], [11]), - ("wasmq rule 12: past 2^15 qubits", wasmq_job(result={"qubits": 2 ** 15 + 1}), ["wasmq_form"], [12]), - ("wasmq rule 12: the transcript's digest in upper case", wasmq_job(result={"transcript_blake3": "F6" * 32}), - ["wasmq_form"], [12]), - ("wasmq rule 13: fuel a member more", wasmq_job(result={"fuel__note": "x"}), ["wasmq_form"], [13]), - ("wasmq rule 13: a budget not run's", wasmq_job(result={"fuel__budget__per_call": 400}), - ["wasmq_form"], [13]), - ("wasmq rule 13: used past per_job", wasmq_job(request={"wasmq__run__fuel__per_job": 6899}, - result={"fuel__budget__per_job": 6899}), - ["wasmq_form"], [13]), - ("wasmq rule 14: a member no extern names", wasmq_job(result={"fuel__callouts__plan": _DELETE, - "fuel__callouts__plan2": plan_fuel}), - ["wasmq_form"], [14]), - ("wasmq rule 14: max past its fuel", wasmq_job(result={"fuel__callouts__plan__max": 120}), - ["wasmq_form"], [14]), - ("wasmq rule 14: fuel past calls × max", wasmq_job(result={"fuel__callouts__decode64__max": 199}), - ["wasmq_form"], [14]), - ("wasmq rule 14: max past per_call", wasmq_job(request={"wasmq__run__fuel__per_call": 150}, - result={"fuel__budget__per_call": 150}), - ["wasmq_form"], [14]), - ("wasmq rule 14: no calls", wasmq_job(result={"fuel__callouts__plan": {"calls": 0, "fuel": 0, "max": 0}, - "fuel__used": 6800, "callouts": 32}), - ["wasmq_form"], [14]), - ("wasmq rule 15: used not the sum", wasmq_job(result={"fuel__used": 6901}), ["wasmq_form"], [15]), - ("wasmq rule 15: callouts not the calls", wasmq_job(result={"callouts": 35}), ["wasmq_form"], [15]), - ("wasmq rule 16: a module named otherwise", wasmq_job(result={"modules__1__name": "other"}), - ["wasmq_form"], [16]), - ("wasmq rule 16: a module missing", wasmq_job(result={"modules__1": _DELETE}), ["wasmq_form"], [16]), - ("wasmq rule 16: a module a member more", wasmq_job(result={"modules__0__bytes": 39442}), - ["wasmq_form"], [16]), - ("wasmq rule 16: another digest", wasmq_job(result={"modules__0__blake3": "d4" * 32}), - ["wasmq_form"], [16]), - ("wasmq rule 17: instances past M × S × per_instance", wasmq_job( - request={"wasmq__run__fuel__per_instance": 2}, result={"fuel__budget__per_instance": 2}), - ["wasmq_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}), ["wasmq_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}), - ["wasmq_form"], [17]), - ("wasmq rule 18: ok, a shot not run", wasmq_job(result={"shots_run": 31, "counts__0101": 29}), - ["wasmq_form"], [18]), - ("wasmq rule 18: ok, with a fault", wasmq_job(result={"fault": { - "reason": "unreachable", "shot": 32, "phase": "seed", "module": "decoder", "fuel": 1}}), - ["wasmq_form"], [18]), - ("wasmq rule 18: ok, with a fault of null", wasmq_job(result={"fault": None}), ["wasmq_form"], [18]), - ("wasmq rule 18: another status", wasmq_job(result={"status": "done"}), ["wasmq_form"], [18]), - ("wasmq rule 18: fault, and no fault", wasmq_job(fault=True, result={"fault": _DELETE}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a reason §6.2 does not name", wasmq_job(fault=True, result={"fault__reason": "timeout"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: every shot run", wasmq_job(fault=True, result={ - "shots_run": 8, "counts__0101": 7, "fault__shot": 8}), ["wasmq_form"], [18]), - ("wasmq rule 18: the fault's shot not shots_run", wasmq_job(fault=True, result={"fault__shot": 1}), - ["wasmq_form"], [18]), - ("wasmq rule 18: another phase", wasmq_job(fault=True, result={"fault__phase": "run"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a module not the contract's", wasmq_job(fault=True, result={"fault__module": "other"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: fuel past per_call in a call", wasmq_job(fault=True, result={"fault__fuel": 401}), - ["wasmq_form"], [18]), - ("wasmq rule 18: fuel past F's used", wasmq_job(fault=True, result={ - "fault__phase": "init", "fault__budget": "instance", "fault__callout": _DELETE, "fault__fuel": 1401}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a call without its callout", wasmq_job(fault=True, result={"fault__callout": _DELETE}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a callout no extern names", wasmq_job(fault=True, result={"fault__callout": "plan2"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a callout outside a call", wasmq_job(fault=True, result={ - "fault__phase": "init", "fault__budget": "instance"}), ["wasmq_form"], [18]), - ("wasmq rule 18: fuel_exhausted without its budget", wasmq_job(fault=True, result={"fault__budget": _DELETE}), - ["wasmq_form"], [18]), - ("wasmq rule 18: a budget on a trap", wasmq_job(fault=True, result={"fault__reason": "unreachable"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: an instance's budget in a call", wasmq_job(fault=True, result={"fault__budget": "instance"}), - ["wasmq_form"], [18]), - ("wasmq rule 18: the fault a member more", wasmq_job(fault=True, result={"fault__note": "x"}), - ["wasmq_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"}), ["wasmq_form"], [18]), - ("wasmq rule 18: a call's fuel past its callout's max", wasmq_job(fault=True, result={ - "fuel__callouts__decode64__max": 399}), ["wasmq_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}), ["wasmq_form"], [18]), - ("wasmq rule 18: a callout of null outside a call, present", wasmq_job(fault=True, result={ - "fault__phase": "init", "fault__budget": "instance", "fault__callout": None}), ["wasmq_form"], [18]), - ("wasmq rule 18: a budget of null on a trap, present", wasmq_job(fault=True, result={ - "fault__reason": "unreachable", "fault__budget": None}), ["wasmq_form"], [18]), - ("wasmq rule 19: outcomes of two lengths", wasmq_job(result={"counts": {"0101": 30, "110": 2}}), - ["wasmq_form"], [19]), - ("wasmq rule 19: an outcome not of 0 and 1", wasmq_job(result={"counts": {"0101": 30, "1102": 2}}), - ["wasmq_form"], [19]), - ("wasmq rule 19: a count of 0", wasmq_job(result={"counts": {"0101": 32, "1100": 0}}), - ["wasmq_form"], [19]), - ("wasmq rule 19: counts not shots_run", wasmq_job(result={"counts__0101": 29}), ["wasmq_form"], [19]), - ("wasmq rule 19: counts an array", wasmq_job(result={"counts": []}), ["wasmq_form"], [19]), - # 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]), - ) - for name, raw_case, want, rules in wasmq_cases: - check(name, reasons_of(raw_case) == want) - if rules is not None: - check(name + ": its rules", rules_of(raw_case) == rules) - alone = set(rules[0] for _, _, want, rules in wasmq_cases if want == ["wasmq_form"] and len(rules) == 1) - check("wasmq: each of rules 2 to %d broken alone" % WASMQ_RULES, alone == set(range(2, WASMQ_RULES + 1))) - wasmq_run = wasmq_job() - check("wasmq: changed after its seal, the form kept: digest_mismatch alone", reasons_of(changed_after( - wasmq_run, lambda job: job["result"].update(qubits=18))) == ["digest_mismatch"]) - check("wasmq: changed after its seal, the form broken: beside", reasons_of(changed_after( - wasmq_run, lambda job: job["result"].update(profile="x"))) == ["digest_mismatch", "wasmq_form"]) - check("wasmq: no receipt, alone", reasons_of(changed_after( - wasmq_run, lambda job: (job["result"].pop("receipt"), job["result"].update(profile="x")))) - == ["no_receipt"]) - check("wasmq: not a job, alone", reasons_of(changed_after( - wasmq_run, lambda job: job.update(request=[]))) == ["not_a_job"]) - wasmq_asked, wasmq_made = wasmq_parts(False) - check("wasmq: a set's item, valid", reasons_of(_set_body( - seed, {"programs": [wasmq_asked]}, [wasmq_asked], item_result=_dump(wasmq_made))) == []) - check("wasmq: a set's item breaking a rule", reasons_of(_set_body( - seed, {"programs": [wasmq_asked]}, [wasmq_asked], - item_result=_dump(dict(wasmq_made, profile="x")))) == ["set_item"]) - - # 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. - def statement(outcome, problems=(), notes=()): - return {"outcome": outcome, "problems": list(problems), "notes": list(notes)} - - sealed_line = '; the sealed result says ' - statement_cases = ( - ("a run", wasmq_job(), [], statement("ok")), - ("a run stopped by a fault", wasmq_job(fault=True, envelope='"status":"fault","method":"stabilizer",'), - [], statement("fault")), - ("a job breaking one rule", wasmq_job(result={"fuel__callouts__plan__max": 120}), ["wasmq_form"], - statement("ok", ["rule 14: " + WASMQ_RULE_WORDS[14]])), - ("a call fault past its callout's max", wasmq_job( - fault=True, envelope='"status":"fault","method":"stabilizer",', - result={"fuel__callouts__decode64__max": 399}), ["wasmq_form"], - statement("fault", [_rule_words(18, {"status": "fault"})])), - ("a fault's unsealed status says done: valid, one note", wasmq_job(fault=True), [], - statement("fault", notes=['the host\'s unsealed status says "done"' + sealed_line + '"fault"'])), - ("the unsealed status and method changed", wasmq_job(envelope='"status":"failed","method":7,'), [], - statement("ok", notes=[ - 'the host\'s unsealed status says "failed"' + sealed_line + '"ok", and the body carries "done" for that', - 'the host\'s unsealed method says 7' + sealed_line + '"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' + sealed_line + '"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}), ["wasmq_form"], - statement(None, ["rule 18: the status is neither ok nor fault"], - ['the host\'s unsealed status says "done"' + sealed_line + '7, and the body carries "fault" for that'])), - ("a sealed status missing: outcome null", wasmq_job( - result={"status": _DELETE}, envelope='"status":"fault","method":"stabilizer",'), ["wasmq_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"' + sealed_line + '"provider"'])), - ) - 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"}))) - - # In every case of the table above: a wasmQ job's problems are the rules it breaks, one - # entry a rule, in order, and there are some exactly when wasmq_form is reported; - # another job states nothing. 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) + check("wasmq: each of rules 2 to %d broken alone" % WASMQ_RULES, alone == set(range(2, WASMQ_RULES + 1))) 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([]) == []) - # Reported alone, a job's form is not read, and it states nothing. - duplicated_wasmq = wasmq_run.replace(b'{"status"', b'{"status":"x","status"') - check("wasmq statement: unreadable states none", judge(duplicated_wasmq) == _verdict(["unreadable"])) - check("wasmq statement: no_receipt states none", judge(changed_after( - wasmq_run, lambda job: job["result"].pop("receipt"))) == _verdict(["no_receipt"])) - check("wasmq statement: not_a_job states none", judge(changed_after( - wasmq_run, lambda job: job.update(request=[]))) == _verdict(["not_a_job"])) + wasmq_asked, wasmq_made = _wasmq_parts(False) check("wasmq statement: a set holding a wasmQ job states none", "wasmq" not in judge(_set_body( - seed, {"programs": [wasmq_asked]}, [wasmq_asked], item_result=_dump(wasmq_made)))) + _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 @@ -2889,7 +3427,7 @@ 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_job(result={"profile": "wai.quantum.wasmq/2"}), ["wasmq_form"], []), + ("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) @@ -2897,23 +3435,23 @@ def self_test(args): 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_job(result={"profile": "wai.quantum.wasmq/2"}) + 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") == statement("ok", ["rule 11: " + WASMQ_RULE_WORDS[11]])) + 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 == statement("ok")) + unchecked_statement == _wasmq_statement("ok")) PLANTS.add("wasmq-unchecked") try: - unchecked_notes = judge(wasmq_job(fault=True)).get("wasmq") + unchecked_notes = judge(_wasmq_job(fault=True)).get("wasmq") finally: PLANTS.discard("wasmq-unchecked") - check("plant wasmq-unchecked: notes still given", unchecked_notes == statement( - "fault", notes=['the host\'s unsealed status says "done"' + sealed_line + '"fault"'])) + 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..0f37598e2 --- /dev/null +++ b/wai/quantum-conformance/tools/wasmq_mutants.py @@ -0,0 +1,536 @@ +#!/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`. + +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 = { + "1|drop|rule 1 never fails": "every place a number may stand in a wasmQ job has a rule of its own that holds it within 2^53 - 1 (a count, an input, a bound), so no job breaks rule 1 and no other", + "1|drop|the check never fails: `past_exact(value)`": "the same, in the engine", + "6|bound|GtE for Gt in len(modules) > 0": "rule 7 holds a contract with no module already: its callouts, of which there is one at least, each name a module below the modules' number, and none is below 0", + "7|bound|GtE for Gt in len(callouts) > 0": "rule 7 holds a contract with no callout already: each module's index is the module of a callout, and rule 6 gives one module at least", + "22|bound|GtE for Gt in len(stopped_in) > 0": "rule 18 holds a call's fault to a callout that is the extern of one of C's callouts already, so a call's fault with no such callout breaks rule 18", +} + + +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): + 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(") +_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 = [] + for kind, what, mut in variants: + k = len(mutants) + 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 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/envelopes/bits-nothing-writes.wai b/wai/quantum-conformance/wasmq/envelopes/bits-nothing-writes.wai new file mode 100644 index 0000000000000000000000000000000000000000..8461ab005b50d6962d7ebc010429bf264b1b8755 GIT binary patch literal 820 zcmZWnO>fjN5KRgcDg24ZCn_zAon-T|D=PKCfkT0cLl3K{*ztI4nmAc+qIQYWKgXH> z#CW%W5V^$j{AT9)&0Jr6U_U(18_7M_WJ%bZk{M}0YL1c6hTA;?+Vo(0L@ac*u6PfQ z)k>&uENlmDp*h@-q~aYHy4L+3*;sSF9k}U-W`56i%@(s1uWP}pJF+xGT~9MowMOb* zw?>-{S&rmR8+piee7x_u-2nC;$#xLB-;pJok=ejkurR&|< zR%xf?f79Nw1H0W=-Z&fnNK5jh;%Qn$h05hZh$K-NU1UiWt3=W$lcGqgB8t;Wu_&Ub zn@W~LtkRg~=_2|ojnTplxjPs(PUyNF(4vv30c!W}z}`4Z(R_ORP%>iL?0Rl0)cqT$ z$BS>RKrCZfk*5XA6D-+4f7`mzVv1Xl?_g2E)BOG!Y3~*cc{pS;O`kr$|9ExrTM7+?RD!u7bnntXUauSAoOgGUUfo tREBmH*zg@@#%HU*!!^YP^88c(tN-XfK0EQfmoI!@d0uepKleiK)i2V-_9XxS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..897cdc966c0eda1cd5fad42093bd87e0ce4afb1d GIT binary patch literal 911 zcmZWoO>Yx15Zx_b(()%PpIRvdJK4{yK}Zkh;~%f|a3%;RmfG*g$+CK;{hZNa}YnUUe5-|giX%@$Y5-gwd zJV}alCRxVgIn+hX1y?f}&+8eh6|%WXCCEbNQl;SSly}ZoohK7&M2p>Mi|EGc&iZC6 z-+N;_rM;5>O?%Dv>~3vE>uvZg4S1Z2G*_8az*7)1=1gU?yh?bI7l6|-EZq>!po zb;c!2VZ(^7+a4_%sOq8d{~NHE-cmH59eyYoF>Q8Em0 z%%`l3w5giD27*=UHS%^-L}(cu#!A#N?#Pm_PROgb!pBAYqv6CiFd)fmB4hyUh+Cx<>{up=7CQj-m+FhdX=eY8t z7`FupkxM+!Z)Tp~%MM$FCN9A-eZ;#RV9UbAS>5b)i@`raoTpa zan|j~Y9J5R>0@r-<9#Rm4zTw?_Pw;-fvotP9PXQNVt4{TdNGb)CQ)>Ib4`E6;W-N1 zG=1P~1_DoQ6E$`tHKMJzjStPXp?B*W zV}p|aO?yj^>}Kah9c=h3EvFn(!!pe>&SbLWzzQm;p@q(rWMyeeDvKnAT%?j^W|Bu9;)zf!QTZ($%-bY4;3S3-Jugsq3l0+a=Q4| zNW= 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 | @@ -1110,7 +1110,7 @@ Each construct below is supported (read as the language defines it), lowered (re | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | -| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q130 | #### Timing @@ -1230,6 +1230,7 @@ Every code: | `Q127` | refused | An assignment to a constant, an input or a loop variable. | | `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). | | `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. | +| `Q130` | refused | Statements, operators and brackets nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block, and each operand of an operator or inside a bracket, is one deeper than what holds it. | | `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. | | `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). | | `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. | @@ -1239,7 +1240,7 @@ Every code: | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16 written in standard gates, 255 on a gate a method holds whole). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000), or that holds gates whole on more qubits than that in all. | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -1267,6 +1268,7 @@ Every code: | `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-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index d13fbe80d..72725a2ed 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21723,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" }, @@ -21763,6 +21770,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 c0d2d431d..d0a1b258d 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -31811,8 +31811,11 @@ "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" @@ -31928,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" }, 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 c085cff2a..a0e5340be 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -795,15 +795,14 @@ fn at_inputs(text: String, inputs: &wai_quantum::qasm::Inputs) -> Result) -> Result, String> { +/// A job's bytes, read only when they are within [`MAX_JOB_BYTES`]: `Ok(None)` for more. +fn read_job_bytes(file: Option<&Path>) -> Result>, String> { use std::io::Read as _; - let past = |what: &str| format!("{what} holds more than the {MAX_JOB_BYTES} bytes a job takes"); let bytes = match file { Some(p) if p != Path::new("-") => { let f = std::fs::File::open(p).map_err(|e| format!("{}: {e}", p.display()))?; if f.metadata().map_err(|e| format!("{}: {e}", p.display()))?.len() > MAX_JOB_BYTES { - return Err(past(&p.display().to_string())); + return Ok(None); } let mut b = Vec::new(); f.take(MAX_JOB_BYTES + 1).read_to_end(&mut b).map_err(|e| format!("{}: {e}", p.display()))?; @@ -815,10 +814,7 @@ fn read_job_bytes(file: Option<&Path>) -> Result, String> { b } }; - if bytes.len() as u64 > MAX_JOB_BYTES { - return Err(past("the job")); - } - Ok(bytes) + Ok((bytes.len() as u64 <= MAX_JOB_BYTES).then_some(bytes)) } fn read_text(file: Option<&Path>) -> Result { @@ -1499,7 +1495,20 @@ fn main() { #[cfg(feature = "service")] Command::Task { cmd, args } => task::main(cmd, &args), Command::Verify { file, history: hist, bundle, json: as_json, lines } => { - let bytes = read_job_bytes(file.as_deref()).unwrap_or_else(|e| die(&e)); + 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 {MAX_JOB_BYTES} bytes verify reads")); + } + if as_json { + println!("{}", job::too_large(MAX_JOB_BYTES)); + } else { + println!("signature INVALID (too_large)"); + not_judged(job::TOO_LARGE, bundle.is_some()); + } + std::process::exit(1); + }; 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 @@ -1526,25 +1535,25 @@ fn main() { 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); + let c = job::check_bytes(e.as_bytes()); + say_alone(&c, as_json, bundle.is_some()); } }; - 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); - } - die("not JSON"); - }; + let c = job::check_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 = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}"))); // 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. + // 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. @@ -1564,97 +1573,73 @@ fn main() { 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 own_verifier && v["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - if !estimate::verify(&v) { - std::process::exit(1); - } - } else if own_verifier { - 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; - } - // The envelope is run again only for a job that was judged: one refused as - // unreadable or with no receipt is reported alone (section 8.4). - #[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; - } + 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_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; } - 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 } => { @@ -1770,6 +1755,34 @@ 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. +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 1 GiB 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 835a2e305..707d645e0 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -622,23 +622,18 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli } "verify_quantum_result" => { 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 job::has_own_verifier(&parsed) { - 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 job::has_own_verifier(given) { - return if given["result"]["format"] == wai_quantum::engine::estimate::FORMAT { wai_quantum::engine::estimate::verify(given) } else { 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)), @@ -1007,7 +1002,8 @@ pub fn serve(memory_gb: u64, key: &SigningKey) { if line.trim().is_empty() { continue; } - let reply = match serde_json::from_str::(&line) { + // Read with room around a job as deep as a reader takes (job::read_message). + let reply = match job::read_message(line.as_bytes()) { 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}") } })), }; diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index 3d05ee720..6b0efaaa7 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -298,17 +298,7 @@ pub fn report(c: &wai_quantum::engine::job::Check, v: &Value, bundle: Option<&Pa // (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 { - let why = match c.reasons.first().copied() { - Some("unreadable") => "its text is unreadable", - Some("no_receipt") => "its result carries no receipt", - Some("not_a_job") => "it is not a job", - Some(other) => other, - None => "it was not read", - }; - println!("result not judged: {why}, which is reported alone (the provider protocol's section 8.4)"); - if bundle.is_some() { - println!("bundle not run: the job was not judged"); - } + crate::not_judged(c.reasons.first().copied().unwrap_or(""), bundle.is_some()); return true; }; let r = &v["result"]; diff --git a/wai/wai-quantum-cli/tests/verify.rs b/wai/wai-quantum-cli/tests/verify.rs index 957783dee..a657aa4a3 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 }; @@ -278,3 +279,92 @@ fn a_wasmq_job_naming_another_schema_is_judged_as_a_wasmq_job() { 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")); +} diff --git a/wai/wai-quantum-cli/tests/wasmq.rs b/wai/wai-quantum-cli/tests/wasmq.rs index ac762618d..587d90dc9 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -416,11 +416,12 @@ fn json_and_lines_give_a_wasmq_job_s_verdict_as_json() { } } -// On Linux, where a sparse file costs nothing: a job file past any job's size is refused by -// its size, before it is read whole and before its kind is known. +// 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_refuses_a_file_past_any_job_s_size_before_it_is_read() { +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(); @@ -428,9 +429,13 @@ fn verify_refuses_a_file_past_any_job_s_size_before_it_is_read() { if file.set_len(1 << 42).is_err() { return; // this file system holds no file so long } - // Refused for the file's length, which names it: not read, then found too 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!(1u64 << 30), "{said}"); let o = run(&d, &["verify", path.to_str().unwrap()]); - assert!(!o.status.success() && text(&o).contains(&format!("{} holds more than", path.display())), "{}", text(&o)); + 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: diff --git a/wai/wai-quantum-py/python/wai_quantum/_api.py b/wai/wai-quantum-py/python/wai_quantum/_api.py index eaa3e1848..6a5814767 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -209,31 +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))) - # Text, a byte order mark before it among what it may begin with: read as text, where - # the mark makes it unreadable, never taken for a path. - if isinstance(job, str) and job.lstrip(" \t\r\n\ufeff").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, and a - # number past 64 bits is a number, read as its double (section 8.2). + # 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=False) - except RecursionError: + 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("utf-8"))) + 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 dde09378e..e50c3d50d 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -590,6 +590,30 @@ def test_a_dict_is_judged_as_its_text_is(): 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-service/README.md b/wai/wai-quantum-service/README.md index 5b8a2392f..da7f6d67f 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. A body that does not read as a job's, is no job, or has no receipt gets that reason as its verdict | +| `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 64 MiB it reads (`too_large`) gets that reason as its verdict. 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/src/discovery.rs b/wai/wai-quantum-service/src/discovery.rs index e51f1dc9b..87ef63a66 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 (what 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 (too_large, with read_at_most) 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\"} for an estimate; {\"valid\", \"reasons\", \"signed\", \"chain\", \"rerun\"} for an algorithm's answer); 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, or an estimate's or an algorithm's request is not one the service runs again (the reason says which)" }, + "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", diff --git a/wai/wai-quantum-service/src/http.rs b/wai/wai-quantum-service/src/http.rs index a8cd16e97..ff3322711 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -103,6 +103,10 @@ 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 job of the largest runs (a +/// wasmQ job of 2^20 shots of many outcomes holds megabytes of counts). A longer one is +/// `too_large`, a verdict like any other (the provider protocol's section 8.4). +pub const VERIFY_BODY: usize = 64 << 20; /// How long `POST /v1/run` waits before answering 202. pub const RUN_WAIT: Duration = Duration::from_secs(25); @@ -202,6 +206,7 @@ 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, "job_memory_bytes": c.job_mib << 20, "job_seconds": c.job_timeout.as_secs(), "active_jobs_per_client": c.per_client_active, @@ -248,6 +253,7 @@ 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, }) } @@ -562,37 +568,51 @@ 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. A body that does not read, is no job or -/// has no receipt gets that reason as its verdict. 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") }; - // Text that does not read as a job's (§8.2), a body that is no job, a result with no - // receipt: each is a verdict with that reason alone (§8.4), as every surface gives it. - let Some(text) = std::str::from_utf8(&bytes).ok() else { return reply(200, wai_quantum::engine::job::check_bytes(&bytes).to_json()) }; - let Ok(v) = serde_json::from_str::(text) else { return reply(200, wai_quantum::engine::job::check_text(text).to_json()) }; - // Read as every job's text is, whatever kind of job it is. - if wai_quantum::engine::job::has_own_verifier(&v) && 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] })); + let bytes = match body { + Ok(b) => b, + Err(e) if e.status() == StatusCode::PAYLOAD_TOO_LARGE => return reply(200, wai_quantum::engine::job::too_large(VERIFY_BODY as u64)), + Err(e) => return error(400, &format!("the body could not be read: {e}")), + }; + 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), } - // An estimate: its seal, every resource receipt, and its request re-run - // here, within an estimate's bounds. - if wai_quantum::engine::job::has_own_verifier(&v) && 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 job as a surface is given it: its bytes, or its members already parsed. +pub(crate) enum Given<'a> { + Bytes(&'a [u8]), + 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. +pub(crate) async fn verdict(app: &App, who: &str, given: Given<'_>) -> Result { + use wai_quantum::engine::job; + 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::Members(v) => job::answer_of(&v), + }; + let j = match answer { + job::Answer::Verdict(v) => return Ok(v), + job::Answer::Rerun(j) => j, + }; + if j["result"]["format"] == wai_quantum::engine::estimate::FORMAT { + crate::analysis::estimate_bounds(&j["request"])?; + } else { + crate::analysis::variational_bounds(j["result"]["call"].as_str().unwrap_or(""), &j["request"])?; } - // 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 }))); - } - reply(200, wai_quantum::engine::job::check_text(text).to_json()) + crate::analysis::bounded(app, who, move || job::rerun(&j)).await } /// A design check ([`crate::analysis`]): the engine's answer, or the refusal. @@ -753,9 +773,19 @@ 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: Result) -> Response { + let bytes = match body { + Ok(b) => b, + Err(e) if e.status() == StatusCode::PAYLOAD_TOO_LARGE => { + return reply(413, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": format!("the message is longer than the {VERIFY_BODY} bytes this server reads; give a job past it to `wai-quantum verify`, which reads 1 GiB") } })); + } + Err(e) => return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("the message could not be read: {e}") } })), + }; + // Read with room around a job as deep as a reader takes (job::read_message), so that it + // reaches its verdict. + let msg = match wai_quantum::engine::job::read_message(&bytes) { + Ok(msg) => msg, + 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" } })); @@ -872,7 +902,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))) @@ -894,7 +924,7 @@ pub fn router(app: Shared) -> Router { .route("/v1/examples", get(examples)) .route("/v1/targets", get(targets)) .route("/v1/openapi.json", get(openapi)) - .route("/mcp", post(mcp).get(mcp_get)) + .route("/mcp", post(mcp).layer(DefaultBodyLimit::max(VERIFY_BODY)).get(mcp_get)) .route("/", get(index)) .fallback(|| async { error(404, "no such endpoint; GET /v1 lists them") }) .layer(axum::middleware::from_fn_with_state(Arc::clone(&app), guard)) diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index ea2b54115..bfbb34aca 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" }, "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" } } }, + "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 } }, { @@ -801,42 +801,36 @@ 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::Bytes(text.as_bytes()), + 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) => { + 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 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) + } 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" => { - // The job's members as given, the host's unsealed ones among them, checked as every - // surface checks a job (`job::verdict`): what is sealed decides, and where the - // body's other members differ from it, a wasmQ job's verdict notes it. - let mut body = args.as_object().cloned().unwrap_or_default(); - body.remove("job"); - let v = wai_quantum::engine::job::verdict(&Value::Object(body)).unwrap_or_else(|e| json!({ "valid": false, "error": e })); - 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 { format!("not valid: {}", v["reasons"]) }; - tool_result(text, v, false) - } "quantum_service_info" => { let c = &app.service.config; let (queued, running, _) = app.service.load(); diff --git a/wai/wai-quantum-service/tests/verify.rs b/wai/wai-quantum-service/tests/verify.rs index bcc9e4434..1285a0be4 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") @@ -139,3 +164,72 @@ fn every_body_gets_a_verdict_and_the_tool_reads_the_body_it_is_given() { 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); + // Past what verify reads: too_large, over HTTP; the tool's message past what the server + // reads is refused with the limit it states. + let mut huge = job.clone(); + huge["pad"] = json!("x".repeat(65 << 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"] != json!(64 << 20) { + problems.push(format!("a job past 64 MiB: {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")); +} diff --git a/wai/wai-quantum-views/src/lib.rs b/wai/wai-quantum-views/src/lib.rs index 324e6d5f4..2def1b65f 100644 --- a/wai/wai-quantum-views/src/lib.rs +++ b/wai/wai-quantum-views/src/lib.rs @@ -35,7 +35,8 @@ 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) { + // Read with room around a job as deep as a reader takes, so that it reaches its verdict. + let req: Value = match job::read_message(request.as_bytes()) { Ok(v) => v, Err(e) => return json!({ "error": format!("the request is not JSON: {e}") }).to_string(), }; @@ -172,6 +173,23 @@ mod tests { 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"])); + } + #[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 38446ee24..ca21c07ec 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -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 9b098e831..5a0d3ff65 100644 --- a/wai/wai-quantum/docs/qasm-diagnostics.md +++ b/wai/wai-quantum/docs/qasm-diagnostics.md @@ -79,6 +79,7 @@ engine. A test holds this page to the code's table. | `Q127` | refused | An assignment to a constant, an input or a loop variable. | | `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). | | `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. | +| `Q130` | refused | Statements, operators and brackets nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block, and each operand of an operator or inside a bracket, is one deeper than what holds it. | | `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. | | `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). | | `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. | @@ -88,7 +89,7 @@ engine. A test holds this page to the code's table. | `Q206` | lowered | `gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls. | | `Q207` | refused | A control modifier that adds no control: `ctrl(0) @`. | | `Q208` | refused | Modifiers that add more controls than the reader lowers (16 written in standard gates, 255 on a gate a method holds whole). | -| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128. | +| `Q209` | refused | Gate definitions or subroutine calls nested deeper than 32. | | `Q210` | refused | A program that expands to more gates than the reader holds (2 000 000), or that holds gates whole on more qubits than that in all. | | `Q211` | refused | A modifier on something that is not a gate. | | `Q212` | refused | A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading. | @@ -116,6 +117,7 @@ engine. A test holds this page to the code's table. | `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 6391b71f1..aa3d096e6 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 | @@ -149,7 +149,7 @@ and in the browser build. | Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 | | A subroutine called as a gate (`f(1) q, r;`) | supported | | | Recursion | refused past 32 calls deep | Q209 | -| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q209 | +| Statements, operators and brackets nested more than 128 deep | refused, before a reader's stack could run out | Q130 | ## Timing 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..ff54b54d7 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" }, diff --git a/wai/wai-quantum/src/engine/estimate/mod.rs b/wai/wai-quantum/src/engine/estimate/mod.rs index 7f669d35f..189446a7d 100644 --- a/wai/wai-quantum/src/engine/estimate/mod.rs +++ b/wai/wai-quantum/src/engine/estimate/mod.rs @@ -856,6 +856,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 ee95d88e6..847c4b682 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -140,6 +140,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"; @@ -215,6 +218,84 @@ 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 { + let mut most = 0; + let mut stack = vec![(v, 1usize)]; + while let Some((v, depth)) = stack.pop() { + match v { + Value::Array(a) => { + most = most.max(depth); + stack.extend(a.iter().map(|x| (x, depth + 1))); + } + Value::Object(o) => { + most = most.max(depth); + stack.extend(o.values().map(|x| (x, depth + 1))); + } + _ => {} + } + } + 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()), + } +} + +/// What a surface that reads at most `limit` bytes of a job finds of a longer one: +/// `too_large`, reported alone (§8.4). +pub fn too_large(limit: u64) -> Value { + let mut v = Check::only(TOO_LARGE).to_json(); + v["read_at_most"] = json!(limit); + v +} + +/// 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 { @@ -256,6 +337,60 @@ 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 { + let c = check_text(text); + if alone(&c) { + return Answer::Verdict(verdict_of(&c)); + } + let Ok(job) = serde_json::from_str::(text) else { return Answer::Verdict(verdict_of(&c)) }; + answer_checked(job, &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). fn is_set(job: &Value) -> bool { job["result"]["format"] == super::sets::FORMAT @@ -322,6 +457,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); }; @@ -597,6 +736,35 @@ pub(crate) mod tests { } } + /// 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/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 7d704f67a..280e62b48 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -54,6 +54,16 @@ pub const MAX_CODE_BYTES: usize = 1 << 19; 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; @@ -90,19 +100,19 @@ fn is_digest(v: &Value) -> bool { } /// Holds `v` to exactly these members: a message for each it lacks, or holds besides. -fn members(v: &Value, what: &str, required: &[&str], optional: &[&str], out: &mut Vec) { +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!("{what} is not an object")); + out.push(format!("rule {rule}: {what} is not an object")); return; }; for name in required { if !o.contains_key(*name) { - out.push(format!("{what} has no `{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!("{what} holds `{name}`, which the text does not define")); + out.push(format!("rule {rule}: {what} holds `{name}`, which the text does not define")); } } } @@ -142,95 +152,107 @@ fn past_exact(v: &Value) -> Option { /// 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) { - let mut problem = |s: String| out.push(format!("the contract: {s}")); + // 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(); - members(c, "it", &["profile", "program", "modules", "callouts", "run"], &[], &mut sub); - members(&c["program"], "its program", &["blake3", "bytes", "inputs"], &[], &mut sub); - members(&c["run"], "its run", &["shots", "seed", "method", "state", "fuel"], &[], &mut sub); - members(&c["run"]["fuel"], "its budgets", &["per_call", "per_job", "per_instance"], &[], &mut sub); + // 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!("its profile is {}, not {PROFILE}", c["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!("its program is {p}, not a digest and a length")); + 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!("its program's inputs are {}, not whole numbers", p["inputs"])); + 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("it holds no module".into()); + problem("rule 6: it holds no module".into()); } let mut code = 0u64; for m in modules { let mut sub = Vec::new(); - members(m, "a module", &["name", "blake3", "bytes", "priors"], &[], &mut sub); + 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_CODE_BYTES as u64) { - problem(format!("a module is {}, not a name, a digest and a length", json!({ "name": m["name"], "blake3": m["blake3"], "bytes": m["bytes"] }))); + 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!("a module's priors are {}", m["priors"])); + problem(format!("rule 6: a module's priors are {}", m["priors"])); } for pr in priors { let mut sub = Vec::new(); - members(pr, "a prior", &["name", "blake3", "bytes"], &[], &mut sub); + 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_ENVELOPE_BYTES as u64) { - problem(format!("a prior is {pr}, not a name, a digest and a length")); + 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!("its modules hold {code} bytes, past {MAX_CODE_BYTES}")); + 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("it names no callout".into()); + 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(x, "a callout", &["extern", "module", "export"], &[], &mut sub); + 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!("a callout is {x}, not an extern, a module it holds and an export")); + 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!("two callouts for the extern {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!("module {k} is named by no callout")); + 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!("its shots are {}, where a run takes 1 to {MAX_SHOTS}", run["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!("its seed is {}", run["seed"])); + problem(format!("rule 8: its seed is {}", run["seed"])); } if run["method"] != METHOD { - problem(format!("its method is {}, where this version of the profile has {METHOD}", run["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!("its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); + problem(format!("rule 8: its state is {}, where an instance lives for a `shot` or the `job`", run["state"])); } - for b in ["per_call", "per_job", "per_instance"] { - if !whole(&run["fuel"][b], 0, MAX_BUDGET) { - problem(format!("its fuel {b} is {}, past what a run may be given", run["fuel"][b])); + // 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])); } } } @@ -239,6 +261,7 @@ fn contract_form(c: &Value, out: &mut Vec) { /// 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() { @@ -246,187 +269,309 @@ pub fn problems(req: &Value, r: &Value) -> Vec { } for (what, value) in [("request", req), ("result", &sealed)] { if let Some(at) = past_exact(value) { - out.push(format!("the {what}'s `{at}` is past 2^53 - 1")); + out.push(format!("rule 1: the {what}'s `{at}` is past 2^53 - 1")); } } - members(req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); + // Rule 2. + members(2, req, "the request", &["wasmq", "envelope_blake3"], &[], &mut out); if !is_digest(&req["envelope_blake3"]) { - out.push(format!("the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); + out.push(format!("rule 2: the request's envelope_blake3 is {}, not a digest", req["envelope_blake3"])); } - members(r, "the result", &RESULT, &["fault"], &mut out); + // 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!("the receipt's canon is {}, not rfc8785", r["receipt"]["canon"])); + 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!("the result's {name} is {}, not {want}", r[name])); + 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 > u64::from(MAX_QUBITS)) { - out.push(format!("the result's qubits are {}", r["qubits"])); + out.push(format!("rule 12: the result's qubits are {}", r["qubits"])); } if !is_digest(&r["transcript_blake3"]) { - out.push(format!("the result's transcript_blake3 is {}, not a 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(fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); + members(13, fuel, "the fuel", &["used", "instances", "budget", "callouts"], &[], &mut out); if !same(&fuel["budget"], &run["fuel"]) { - out.push(format!("the fuel's budget is {}, where the contract gives {}", 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!("fuel used {} is past the run's budget {per_job}", fuel["used"])); + 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!("the fuel of the callouts is {}, not an object", fuel["callouts"])); + 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(f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); + members(14, f, &format!("callout {name}'s fuel"), &["calls", "fuel", "max"], &[], &mut out); if !externs.contains(name.as_str()) { - out.push(format!("fuel for {name}, a callout the contract does not name")); + 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!("callout {name} used {} in one call, past the budget {per_call}", f["max"])); + 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!("callout {name}'s largest call, {}, is past its fuel, {}", f["max"], f["fuel"])); + 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!("callout {name} has fuel for {} calls", f["calls"])); + out.push(format!("rule 14: callout {name} has fuel for {} calls", f["calls"])); } - if all.is_none_or(|a| n.zip(max).is_none_or(|(n, m)| u128::from(a) > u128::from(n) * u128::from(m))) { - out.push(format!("callout {name}'s fuel, {}, is past its {} calls of at most {} each", f["fuel"], f["calls"], f["max"])); + // 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!("fuel used {}, where the instances and the calls add up to {spent:?}", 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!("{} callouts, where the fuel counts {calls:?}", 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("the result's modules are not the ones the contract pins".into()); + out.push("rule 16: the result's modules are not the ones the contract pins".into()); } for m in got { - members(m, "a module of the result", &["name", "blake3"], &[], &mut out); + members(16, m, "a module of the result", &["name", "blake3"], &[], &mut out); if !is_digest(&m["blake3"]) { - out.push(format!("a module of the result whose blake3 is {}, not a 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!("the instances' fuel, {}, is past what {made} of them could be given", fuel["instances"])); + 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("a result that is ok and names a fault".into()); + 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!("a result that is ok after {} of {} shots", r["shots_run"], r["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(f, "the fault", &["reason", "shot", "phase", "module", "fuel"], &["callout", "budget"], &mut out); + 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!("a fault the profile does not name: {}", f["reason"])); + 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!("a fault after {} of {} shots", r["shots_run"], r["shots"])); + 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!("a fault in shot {}, after {} shots finished", f["shot"], r["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!("a fault in a phase the profile does not name: {}", f["phase"])); + 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!("a fault in {}, a module the contract does not 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"]); - if used.is_none_or(|n| count(&fuel["used"]).is_none_or(|all| n > all)) { - out.push(format!("a fault after {} fuel, of {} used", f["fuel"], fuel["used"])); - } let is_call = phase == Some("call"); - let budget = if is_call { per_call } else { per_instance }; - if used.is_none_or(|n| n > budget) { - out.push(format!("a fault after {} fuel, past the {budget} its phase is given", f["fuel"])); + 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!("a fault that names the callout {x} in phase {}", f["phase"])), - None if is_call => out.push("a call's fault that names no callout".into()), + 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!("a budget, {x}, on a fault of {}", f["reason"])), - Some(x) if x != "run" && x != if is_call { "call" } else { "instance" } => out.push(format!("a fuel fault of the budget {x} in phase {}", f["phase"])), - None if reason == Some("fuel_exhausted") => out.push("a fuel fault that names no budget".into()), + 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!("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!("a call of {name:?} that stopped after {} fuel, past the most one of its calls used, {}", f["fuel"], m["max"])), + 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!("status {}", r["status"])), + _ => 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!("the result's counts are {}", r["counts"])); + 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!("an outcome that is not bits: {outcome:?}")); + out.push(format!("rule 19: an outcome that is not bits: {outcome:?}")); } if *width.get_or_insert(outcome.len()) != outcome.len() { - out.push(format!("outcomes of {} and {} bits", width.unwrap_or(0), 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!("a count of {n} for {outcome:?}")); + 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!("counts sum to {total:?}, not the {} shots run", r["shots_run"])); + out.push(format!("rule 19: counts sum to {total:?}, not the {} shots run", r["shots_run"])); } + across(c, r, &mut out); listed(out) } +/// Rules 20 to 31 (§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, 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. + for (name, f) in fuel["callouts"].as_object().into_iter().flatten() { + let mine = u64::from(stopped == Some(name.as_str())); + let returned = count(&f["calls"]).map(|n| n.saturating_sub(mine)); + let theirs = count(&f["fuel"]).and_then(|all| all.checked_sub(mine * fault_fuel.unwrap_or(0))); + let max = count(&f["max"]); + if returned.zip(theirs).zip(max).is_none_or(|((k, used), max)| used < k || u128::from(used) > u128::from(k) * u128::from(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"])); + } + } + // 21: each prior of each instance made whole was passed: a call of cabi_realloc and one + // of wasmq-prior, at least 1 each, and its bytes charged (§5.2, §5.3). + 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 passed: u128 = mods.iter().flat_map(|m| priors(m)).map(|p| bytes(&p["bytes"]) + 2).fold(0, u128::saturating_add); + if count(&fuel["instances"]).is_none_or(|n| u128::from(n) < sets.saturating_mul(passed)) { + out.push(format!("rule 21: the instances' fuel, {}, is less than passing the priors to {sets} sets of instances takes", 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'. + if fault_fuel.zip(count(&fuel["instances"])).is_none_or(|(n, all)| n > all) { + out.push(format!("rule 25: a fault after {} fuel outside a call, past the instances' {}", 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). + let shots = u128::from(count(&run["shots"]).unwrap_or(0)); + if let Some(w) = r["counts"].as_object().into_iter().flatten().map(|(o, _)| o.len() 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 work = shots.saturating_mul(2 * n * n.div_ceil(32).max(1)).saturating_add(instances.saturating_mul(instance)).saturating_add(code).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 frames of the envelope (§2.3). + let frames = mods.iter().flat_map(|m| std::iter::once(&m["bytes"]).chain(priors(m).iter().map(|p| &p["bytes"]))).map(bytes).fold(bytes(&c["program"]["bytes"]), u128::saturating_add); + if frames >= MAX_ENVELOPE_BYTES as u128 { + out.push(format!("rule 31: {frames} bytes of program, modules and priors, past the frames an envelope of {MAX_ENVELOPE_BYTES} bytes holds")); + } +} + /// 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 { @@ -555,7 +700,7 @@ mod tests { }; assert_eq!(with(2), Vec::::new(), "the positive control"); let p = with(1); - assert!(p.len() == 1 && p[0].contains("past the 1 its phase is given"), "{p:?}"); + 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 @@ -568,14 +713,14 @@ mod tests { let mut none = job["result"].clone(); none["callouts"] = json!(0); none["fuel"]["callouts"] = json!({}); - none["fuel"]["used"] = json!(0); - none["fault"]["fuel"] = json!(0); + 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": 400, "max": 200 }); + 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:?}"); @@ -595,4 +740,188 @@ mod tests { } } } + + /// A change to a kept job, and the problems of what it makes. + 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); + 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); + } + + /// Rules 20 to 31: 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 }); + })), + // 21: 32 shots of instances passed a prior of 87 bytes in less than 32 × 89 fuel. + (21, "cli-wasmq-run.json", "passing the priors to 32 sets", ch(|_, r| { + r["fuel"]["instances"] = json!(32 * 89 - 1); + r["fuel"]["used"] = json!(32 * 89 - 1 + 588348); + }), ch(|_, r| { + r["fuel"]["instances"] = json!(32 * 89); + r["fuel"]["used"] = json!(32 * 89 + 588348); + })), + // 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")), ch(|_, r| r["fault"]["reason"] = json!("stack_exhausted"))), + // 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| { + 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!(2); + r["fuel"]["used"] = json!(7); + r["fault"]["shot"] = json!(1); + r["fault"]["fuel"] = json!(2); + })), + // 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 } }); + r["fuel"]["used"] = json!(3); + 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 } }); + r["fuel"]["used"] = json!(3); + r["fault"]["shot"] = json!(2); + })), + // 27: the first shot's instances failing after a call. + (27, "cli-wasmq-fault-init.json", "after 1 callouts", ch(|_, r| { + r["callouts"] = json!(1); + r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 1, "max": 1 } }); + r["fuel"]["used"] = json!(2); + }), ch(|_, _| {})), + // 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; + 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: a prior of 2^27 bytes beside a program and a module; then the most that fits. + (31, "cli-wasmq-fault-prior.json", "past the frames an envelope", ch(|c, _| c["modules"][0]["priors"][0]["bytes"] = json!(1 << 27)), ch(|c, _| { + let rest = c["program"]["bytes"].as_u64().unwrap() + 105; + c["modules"][0]["priors"][0]["bytes"] = json!((1u64 << 27) - rest - 1); + })), + ]; + 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"); + } + } } diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index 1364ab7db..c683f2c56 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -70,7 +70,7 @@ use crate::quantum_tableau::Clifford; /// The most qubits a program declares. pub const MAX_QUBITS: u32 = 65_536; /// The deepest the reader nests statements, operators and brackets: a program nested -/// deeper is refused ([`diag::TOO_DEEP`]) before it could use up a reader's stack, so that +/// deeper is refused ([`diag::NESTING`]) before it could use up a reader's stack, so that /// every host, a small one among them, reads it the same way. pub const MAX_NESTING: u32 = 128; /// The most gates a program expands to. diff --git a/wai/wai-quantum/src/qasm/diag.rs b/wai/wai-quantum/src/qasm/diag.rs index a8cfaab2b..04661d22d 100644 --- a/wai/wai-quantum/src/qasm/diag.rs +++ b/wai/wai-quantum/src/qasm/diag.rs @@ -201,6 +201,7 @@ table! { ASSIGN_CONSTANT = "Q127", Refused, "An assignment to a constant, an input or a loop variable."; MEASURE_PLACE = "Q128", Refused, "A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register)."; NOT_DIFFERENTIABLE = "Q129", Refused, "A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments."; + NESTING = "Q130", Refused, "Statements, operators and brackets nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block, and each operand of an operator or inside a bracket, is one deeper than what holds it."; // Gates and modifiers. GATE_INLINED = "Q200", Lowered, "A `gate` the program defines, inlined at each call with its parameters bound."; @@ -212,7 +213,7 @@ table! { GLOBAL_PHASE = "Q206", Lowered, "`gphase(θ)`: a global phase, which no measurement sees, dropped; under controls it is a phase on the controls."; NO_CONTROLS = "Q207", Refused, "A control modifier that adds no control: `ctrl(0) @`."; TOO_MANY_CONTROLS = "Q208", Refused, "Modifiers that add more controls than the reader lowers (16 written in standard gates, 255 on a gate a method holds whole)."; - TOO_DEEP = "Q209", Refused, "Gate definitions or subroutine calls nested deeper than 32, or statements, operators and brackets nested deeper than 128."; + TOO_DEEP = "Q209", Refused, "Gate definitions or subroutine calls nested deeper than 32."; TOO_MANY_GATES = "Q210", Refused, "A program that expands to more gates than the reader holds (2 000 000), or that holds gates whole on more qubits than that in all."; MODIFIER_PLACE = "Q211", Refused, "A modifier on something that is not a gate."; GATE_BODY = "Q212", Refused, "A statement a gate's body may not hold: a gate holds gate calls, `gphase`, `barrier`, loops and conditions known on reading."; @@ -242,6 +243,7 @@ table! { 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 6f74915f8..726608580 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -1112,6 +1112,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() } }; diff --git a/wai/wai-quantum/src/qasm/lang_tests.rs b/wai/wai-quantum/src/qasm/lang_tests.rs index 1e3405a74..c07558e06 100644 --- a/wai/wai-quantum/src/qasm/lang_tests.rs +++ b/wai/wai-quantum/src/qasm/lang_tests.rs @@ -658,3 +658,26 @@ fn a_callout_s_signature_and_use_are_held_to_what_the_reader_can_run() { // 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"); +} diff --git a/wai/wai-quantum/src/qasm/parse.rs b/wai/wai-quantum/src/qasm/parse.rs index a09834676..dcbf0e87b 100644 --- a/wai/wai-quantum/src/qasm/parse.rs +++ b/wai/wai-quantum/src/qasm/parse.rs @@ -188,7 +188,7 @@ impl<'a> Parser<'a> { fn deeper(&mut self) -> R<()> { self.depth += 1; if self.depth > super::MAX_NESTING { - return self.err(diag::TOO_DEEP, format!("statements and expressions nest deeper than {}", super::MAX_NESTING)); + return self.err(diag::NESTING, format!("statements and expressions nest deeper than {}", super::MAX_NESTING)); } Ok(()) } diff --git a/wai/wai-quantum/tests/wasmq_verdict.rs b/wai/wai-quantum/tests/wasmq_verdict.rs index 61b4079cc..eb1afddaf 100644 --- a/wai/wai-quantum/tests/wasmq_verdict.rs +++ b/wai/wai-quantum/tests/wasmq_verdict.rs @@ -60,7 +60,7 @@ fn a_wasmq_verdict_states_the_run_its_problems_and_the_host_s_words() { // 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("counts sum to"), "{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}"); diff --git a/wai/wai-wasmq/src/envelope.rs b/wai/wai-wasmq/src/envelope.rs index 4cc44d5bc..d15ad9b30 100644 --- a/wai/wai-wasmq/src/envelope.rs +++ b/wai/wai-wasmq/src/envelope.rs @@ -17,7 +17,7 @@ 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::{FUEL_WORK, MAX_BUDGET, MAX_CODE_BYTES, MAX_ENVELOPE_BYTES, MAX_EXACT, MAX_HOST_WORK, MAX_PROGRAM_BYTES, MAX_QUBITS, MAX_SHOTS, METHOD}; +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"; @@ -25,27 +25,16 @@ pub const CAPABILITY: &str = "wai.quantum.wasmq"; 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 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 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 most bits a run's results may 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, as [`host_work`] -/// counts it. -pub const CALL_WORK: u128 = 1 << 12; /// 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; -/// 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). -pub const CODE_WORK: u128 = 1 << 17; /// 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 }; diff --git a/wai/wai-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index f9ef2e96d..e891ba5ec 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -155,6 +155,14 @@ fn more_runs() -> Vec<(&'static str, Bundle)> { // 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)); @@ -268,6 +276,12 @@ fn bundle_refusals() -> Vec<(&'static str, &'static str, Bundle, Bundle)> { 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(); -- 2.54.0 (Apple Git-157) From d82813645001ad3b797340865cb7b30212ee657c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 16:42:36 -0500 Subject: [PATCH 14/17] wasmQ round four: the cases that hold the engine's last weakenings, and their record Seven rules-stage cases where a member's own rule and another's both read it (per_job 2^53 alike in the result, a module's blake3 in upper case alike, a module named 1 read as 1.0 by the result and by a fault, a job-state fault in shot 0.5, a fault in shot 0.5 after a call), which catch the engine's weakenings that only one rule's reading showed. The ten engine weakenings and the one of the second verifier that no input can tell from the text are listed in wasmq_mutants.py, each with its reason. Section 9 names the new vectors and the receipts that break each rule alone. A closure clippy flags is the function itself. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/quantum-wasmq.md | 11 ++++++----- .../tools/receipt_differential.py | 16 ++++++++++++++++ wai/quantum-conformance/tools/wasmq_mutants.py | 17 +++++++++++++++++ wai/wai-quantum/src/engine/wasmq.rs | 2 +- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 07139301a..80a30b845 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -890,8 +890,9 @@ feasible fills the target, and atoms are moved, never made or lost. 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; and a program whose resets and measurements pin §6.1's - stream; + 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 @@ -903,9 +904,9 @@ 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 breaking one rule of §6.5, so the two verifiers of -that corpus, which share no code, are held to each other on `wasmq_form`, and on every -change of one member of each of the six. +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), diff --git a/wai/quantum-conformance/tools/receipt_differential.py b/wai/quantum-conformance/tools/receipt_differential.py index 1e5c6509f..800a95a59 100644 --- a/wai/quantum-conformance/tools/receipt_differential.py +++ b/wai/quantum-conformance/tools/receipt_differential.py @@ -636,6 +636,22 @@ def rule_cases(corpus, seal): # 31: the frames case(31, "frames one short of 2^27", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 - 1 - (frames(b) - contract(b)["modules"][0]["priors"][0]["bytes"])), True) case(31, "frames of 2^27", prior, lambda b: contract(b)["modules"][0]["priors"][0].update(bytes=2 ** 27 - (frames(b) - contract(b)["modules"][0]["priors"][0]["bytes"])), 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) diff --git a/wai/quantum-conformance/tools/wasmq_mutants.py b/wai/quantum-conformance/tools/wasmq_mutants.py index 0f37598e2..ae038a112 100644 --- a/wai/quantum-conformance/tools/wasmq_mutants.py +++ b/wai/quantum-conformance/tools/wasmq_mutants.py @@ -51,6 +51,17 @@ EQUIVALENT = { "6|bound|GtE for Gt in len(modules) > 0": "rule 7 holds a contract with no module already: its callouts, of which there is one at least, each name a module below the modules' number, and none is below 0", "7|bound|GtE for Gt in len(callouts) > 0": "rule 7 holds a contract with no callout already: each module's index is the module of a callout, and rule 6 gives one module at least", "22|bound|GtE for Gt in len(stopped_in) > 0": "rule 18 holds a call's fault to a callout that is the extern of one of C's callouts already, so a call's fault with no such callout breaks rule 18", + # 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-28|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-28|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", } @@ -386,8 +397,14 @@ def _mutate_line(line, following, rule, no, mutants): 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": diff --git a/wai/wai-quantum/src/engine/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 280e62b48..2d19d5152 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -497,7 +497,7 @@ fn across(c: &Value, r: &Value, out: &mut Vec) { // of wasmq-prior, at least 1 each, and its bytes charged (§5.2, §5.3). 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 passed: u128 = mods.iter().flat_map(|m| priors(m)).map(|p| bytes(&p["bytes"]) + 2).fold(0, u128::saturating_add); + let passed: u128 = mods.iter().flat_map(priors).map(|p| bytes(&p["bytes"]) + 2).fold(0, u128::saturating_add); if count(&fuel["instances"]).is_none_or(|n| u128::from(n) < sets.saturating_mul(passed)) { out.push(format!("rule 21: the instances' fuel, {}, is less than passing the priors to {sets} sets of instances takes", fuel["instances"])); } -- 2.54.0 (Apple Git-157) From 849b814dc580430ebaacb6f3acc88af22c123c6f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 10:45:39 -0500 Subject: [PATCH 15/17] wasmQ round 5: the reader's nesting from main, verification within bounds, rules 32 to 35 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review of d8281364 found 24 (3 refuted). Each fix has a test that fails on that head for the reason stated. The shared reader (6, 7, 24) landed on main as its own commit, 15ae7d457, and is merged here. Its §4.8 row now says what it counts: operator, index and `++` chains, brackets in types, `else if` ladders at their `if`'s depth, and bodies read in place of their calls. Every program within Q130 and Q209 reads on a 1 MiB stack, natively and on wasm32. Verification cost (5, 22, 23, 10): - A job is checked with one reading held at a time. The strict tree is dropped before the value is built, and the value is reused by everything after it. - A surface bounds a job's values beside its bytes: one value, plus one for each `,` `:` `[` `{` outside strings, counted in one pass. too_large states both (`read_at_most`: `bytes`, `values`). - `wai-quantum verify` reads 384 MiB and 2^22 values on every host, wasm32 among them, and gives a verdict at that bound. - The service reads 4 MiB and 2^19 values. Its check runs in spawn_blocking, two at a time, together with the bounds check of a re-run's program. - A sealed algorithm answer whose program is a 2000-term sum gets its verdict. The service stays up. Rules (1, 2, 3, 4, 12, 13, 14, 15): - Rule 20 holds the most one call used to a returned call when it exceeds the stopped call's. - Rule 21 counts 3 per prior passed, the export a lifecycle fault shows, and the set the fault stopped. - Rule 25 adds the stopped call's fuel on top. - Rule 29 measures a name in characters. - Rule 30 counts 4097 per call made. - Rule 31 is the envelope's whole length: magic, manifest, contract and frames. - New rules 32 to 35: - 32: the calls per shot (2^21 steps); - 33: a prior passed whole fits one per_instance; - 34: a fault's least fuel by its reason (1; bad_result 2; stack_exhausted 788); - 35: with no call returned, the transcript is the run's end. - Rule 12 takes qubits from 1. - §6.5 says plainly what it leaves free, and why: a program's and a module's least length, and an extern's spelling among them. - The writer takes the transcript's prefix from the engine. - The second verifier was brought to the text by a reader who has not seen the reference. Mutants (16, 17, 18, 19): - A rules case tells P0115 apart. - The generator adds `extreme`, the most of many read as the least, with a case at rule 29 where it shows. - The four false EQUIVALENT entries are gone. Answers (8, 9, 11, 20, 21): - An estimate whose request runs nowhere is not valid and says why (`rerun_error`) on every surface. - `/mcp` reads a message in bounded chunks and answers a verify call past the service's bound with too_large, by its id. - A job given parsed to a view, an MCP tool or the service is `unreadable` past 127, however deep, judged before the message is read whole. - The command line's MCP server answers a line that is not UTF-8 with -32700 and reads on. - A debug build of `run` no longer refuses its own flags: two groups shared one name. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-cli.yml | 18 + ...26-11-05-wasmq-hybrid-quantum-classical.md | 75 ++- wai/extensions/quantum-providers.md | 11 +- wai/extensions/quantum-wasmq.md | 167 ++++-- wai/quantum-conformance/receipts/README.md | 3 +- wai/quantum-conformance/receipts/generate.py | 68 ++- .../crafted-wasmq-bad-result-in-init.json | 12 +- .../jobs/crafted-wasmq-calls-past-steps.json | 110 ++++ .../crafted-wasmq-calls-under-largest.json | 110 ++++ .../jobs/crafted-wasmq-fault-unnamed.json | 6 +- .../crafted-wasmq-fuel-fault-under-limit.json | 6 +- ...rafted-wasmq-job-state-later-instance.json | 8 +- .../receipts/jobs/crafted-wasmq-no-qubit.json | 110 ++++ ...fted-wasmq-prior-past-instance-budget.json | 110 ++++ .../jobs/crafted-wasmq-priors-unpassed.json | 8 +- .../crafted-wasmq-transcript-of-no-call.json | 90 +++ .../jobs/crafted-wasmq-trap-of-no-fuel.json | 90 +++ .../receipts/manifest.json | 54 +- .../tools/receipt_differential.py | 107 +++- .../tools/receipt_verify.py | 548 ++++++++++++++---- .../tools/wasmq_mutants.py | 30 +- wai/wai-quantum-cli/docs/local-mcp.json | 7 + wai/wai-quantum-cli/docs/service-mcp.json | 7 + wai/wai-quantum-cli/src/estimate.rs | 5 +- wai/wai-quantum-cli/src/main.rs | 67 ++- wai/wai-quantum-cli/src/mcp.rs | 76 ++- wai/wai-quantum-cli/src/wasmq.rs | 3 + wai/wai-quantum-cli/tests/verify.rs | 105 ++++ wai/wai-quantum-cli/tests/verify_at_bound.py | 30 + wai/wai-quantum-cli/tests/wasmq.rs | 2 +- wai/wai-quantum-service/README.md | 2 +- wai/wai-quantum-service/docs/self-host.md | 2 +- wai/wai-quantum-service/src/analysis.rs | 42 +- wai/wai-quantum-service/src/discovery.rs | 8 +- wai/wai-quantum-service/src/http.rs | 143 +++-- wai/wai-quantum-service/src/mcp.rs | 40 +- wai/wai-quantum-service/tests/verify.rs | 70 ++- wai/wai-quantum-views/src/lib.rs | 19 +- wai/wai-quantum/schemas/estimate.v1.json | 7 + wai/wai-quantum/src/engine/estimate/mod.rs | 11 +- wai/wai-quantum/src/engine/job.rs | 329 +++++++++-- wai/wai-quantum/src/engine/wasmq.rs | 386 ++++++++++-- wai/wai-wasmq/src/run.rs | 4 +- wai/wai-wasmq/tests/vectors.rs | 20 + 44 files changed, 2665 insertions(+), 461 deletions(-) create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-past-steps.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-calls-under-largest.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-no-qubit.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-prior-past-instance-budget.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-transcript-of-no-call.json create mode 100644 wai/quantum-conformance/receipts/jobs/crafted-wasmq-trap-of-no-fuel.json create mode 100644 wai/wai-quantum-cli/tests/verify_at_bound.py diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml index acf09d0a7..cfee39bcc 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -293,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/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md index c04e887fb..944239e5a 100644 --- a/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md +++ b/wai/changes/2026-11-05-wasmq-hybrid-quantum-classical.md @@ -19,9 +19,9 @@ 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`, a code of - its own, counted from 1 at the top level) and its bounds for programs from others (`Q323`) hold - for every reading of a program; `Q209` keeps its meaning, definitions and calls past 32. + 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 @@ -59,17 +59,17 @@ 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-one numbered rules hold the contract, the result, the fuel, + 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 thirty-eight crafted from them: thirty-five 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), one breaking two, and two the same - sealed content written otherwise or under another envelope. The second verifier, written in + 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. @@ -92,14 +92,29 @@ 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 31 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; each shot's instances passed their priors; 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 within the instances' fuel, in shot 0 for one instance a run, and before any call - in shot 0; a fuel fault used its whole limit; and §4.8's products and sums over what a - receipt shows (a shot's bits, the shots' bits, the work, the frames). §6.5 says what it - leaves free, and why. Rule 6 no longer repeats the bounds rules 6 and 31 imply, and rules 8, +- **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 @@ -114,21 +129,27 @@ 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. `POST /v1/verify` and `/mcp` read 64 MiB; a longer job is - `too_large`, a new reason of §8.4 reported alone, as is a file past the 1 GiB `verify` - reads. A job as deep as a reader takes reaches its verdict through a view's request and an - MCP call, and a parsed job deeper than that is `unreadable`. `verify` prints the verdict + 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 64 MiB - (`too_large`), where it answered 400 for the first three; a client reads `valid` and - `reasons`. Its OpenAPI document says so. -- **Breaking:** every reading of a program refuses statements, operators and brackets nested - deeper than 128 with `Q130`, where earlier versions read them until a reader's stack ran - out (about 1000 deep on an 8 MiB stack, far less on a small one): write a deep expression - as named `const` values, or nest fewer blocks. + 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`. diff --git a/wai/extensions/quantum-providers.md b/wai/extensions/quantum-providers.md index ecdbe2075..98da0c92b 100644 --- a/wai/extensions/quantum-providers.md +++ b/wai/extensions/quantum-providers.md @@ -542,7 +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's text is longer than the surface reads, which it states with the verdict (`read_at_most`, in bytes): it read none of it. Reported alone. `wai-quantum verify` reads 1 GiB, more than any job holds; the job service's `POST /v1/verify` reads 64 MiB | +| `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 | @@ -746,9 +746,12 @@ desktop app and a browser tab. A surface given a job already parsed into objects (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`). A job parsed deeper than a text is read (127) is -`unreadable` there too, and a message that carries a job (a tool's call, a view's -request) is read with room for itself around a job as deep as that, so that the job -reaches its verdict. The Python package judges a dict as the text it writes +`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 diff --git a/wai/extensions/quantum-wasmq.md b/wai/extensions/quantum-wasmq.md index 80a30b845..c68eda5dc 100644 --- a/wai/extensions/quantum-wasmq.md +++ b/wai/extensions/quantum-wasmq.md @@ -374,7 +374,7 @@ compiling its modules, the result. So that an envelope that opens on one host op 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 31). +(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 @@ -383,7 +383,7 @@ a short program cannot make a reader hold or do more than a host has before it i | bound | | |---|---| | its text | 1 MiB (a reader holds some eighty bytes of tokens for each) | -| nesting of statements, operators and brackets | 128 (`Q130`): a reader's stack. A statement at the top level is at depth 1; each statement in a block, and each operand of an operator or inside a bracket, is one deeper than what holds it | +| 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 | @@ -682,7 +682,8 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 of at most 2^15, and its `transcript_blake3` a digest. +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). @@ -719,24 +720,36 @@ request's `wasmq` (the contract), `run` is `C`'s `run`, `r` is the result and `F 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 31 tie members to each other, as the writer does. In them *the fault* is `r`'s +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. Its `fuel` less `s` × the - fault's `fuel` (0 when that is not a count), 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. `calls`, `fuel` and `max` are counts. -21. `F`'s `instances` is a count of at least `W` × `P`. `P` is the sum, over every prior - of every one of `C`'s modules, of 2 and the prior's `bytes` (0 when that is not a - count): passing a prior calls `cabi_realloc` and `wasmq-prior`, each at least 1, and - charges its bytes (§5.2, §5.3). `W`, the shots whose instances were all made, is - `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. + 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 @@ -746,12 +759,14 @@ whose `phase` is `call`, and a *lifecycle fault* one that has a `phase` other th (§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 of at most `F`'s `instances` (a count): what a - lifecycle call used is part of the instances' fuel. +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). + 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 @@ -760,44 +775,86 @@ whose `phase` is `call`, and a *lifecycle fault* one that has a `phase` other th 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 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). +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` + 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; and each sum - is over `C`'s modules, `p` a module's number of priors and `b` its `bytes`. 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 and its fuel. -31. The program's `bytes`, each module's `bytes` and each prior's `bytes`, added (each 0 - when it is not a count), are less than 2^27: the frames an envelope of at most - 128 MiB holds (§2.3). + `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.** Everything else the writer fixes depends on what -no receipt holds, the program's text, the modules' code, the priors' bytes or the run -itself, and only running the envelope again (`--bundle`) holds a job to it: +**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, and each `blake3` against the bytes it pins: they are - digests of what is not sealed; -- `r`'s `qubits`, the outcomes' length and the number of callouts 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 a shot makes, are its own; +- 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`: only its code shows it. So a fault in - phase `seed` or `init` may name any module, and the instances' fuel is bounded above by - rule 17 and below only by its priors' part and a lifecycle fault's own fuel (rules 21 - and 25); -- the fuel of each call beyond rules 14, 18, 20 and 28, and a fault's fuel beyond rules - 18, 20, 25 and 28: where in its work a call traps, and how many bytes its arguments pass, are - its module's and its program's; +- 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 (steps, gates, 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 31 take what a receipt shows, and a job a writer can write - never breaks them. +- 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, @@ -814,9 +871,11 @@ line on its own as a job's text (a line that is not UTF-8, or blank, is that lin 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 1 GiB, more than any job -holds, and gives a file past it `too_large` by its size, before it reads it; a file of -lines past it is refused. With `--bundle ENVELOPE.wai` it also checks +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 diff --git a/wai/quantum-conformance/receipts/README.md b/wai/quantum-conformance/receipts/README.md index 706721450..f57e23e06 100644 --- a/wai/quantum-conformance/receipts/README.md +++ b/wai/quantum-conformance/receipts/README.md @@ -68,7 +68,8 @@ 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), and requires the +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 diff --git a/wai/quantum-conformance/receipts/generate.py b/wai/quantum-conformance/receipts/generate.py index 3f637578c..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("= 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 31 tie members to each other. In them *the fault* is r's fault when r's + # 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 @@ -1337,34 +1339,52 @@ def wasmq_broken(request, result): 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: its fuel less s × the fault's fuel (s 1 when the fault - # is a call's fault whose callout is that member's name, else 0; the fault's fuel 0 when - # it is no count), 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); calls, fuel and max counts. - # "(0 when that is not a count)" is read of the fault's fuel: the member's own fuel is - # held to be a count by the rule's last sentence. - stopped_fuel = _or_zero(fault_fuel) - - def returned_holds(name, e): + # 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 - e_calls, e_fuel, e_max = _get(e, "calls"), _get(e, "fuel"), _get(e, "max") - if not (_count(e_calls) and _count(e_fuel) and _count(e_max)): + calls, fuel, top = _get(member, "calls"), _get(member, "fuel"), _get(member, "max") + if not (_count(calls) and _count(fuel) and _count(top)): return False - returned = _n(e_fuel) - stopped * stopped_fuel - returning = max(_n(e_calls) - stopped, 0) - return returned >= 0 and returning <= returned <= returning * _n(e_max) + 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, e) for name, e in f_callouts.items())) + 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: P, over every prior of every one of C's - # modules, 2 and the prior's bytes (0 when no count); 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. - passing = sum(2 + _or_zero(_get(p, "bytes")) for m in modules for p in _arr(_get(m, "priors"))) + # 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) - rule(21, _count(instances) and _n(instances) >= made_whole * passing) + 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 @@ -1390,9 +1410,11 @@ def wasmq_broken(request, result): # 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 of at most F's instances (a count). + # 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(fault_fuel) <= _n(instances))) + 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) @@ -1430,22 +1452,18 @@ def wasmq_broken(request, result): whole_limit = True rule(28, whole_limit) - # 29. When r's counts has a name, the longest 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). - # SPEC-GAP: [W17] "the length of the longest": the text gives no unit; a name's length - # is read as its characters (Unicode code points), one an outcome's bit. It differs from - # its UTF-8 bytes or UTF-16 units only for a name that is not of 0 and 1, which rule - # 19 refuses. + # 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 + 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); every - # number that is not a count read as 0. + # 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 @@ -1453,14 +1471,66 @@ def wasmq_broken(request, result): 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) + 8 * _or_zero(per_job)) + + 131072 * sum(code) + 4097 * _or_zero(r_callouts) + 8 * _or_zero(per_job)) rule(30, work <= 2 ** 42) - # 31. The program's bytes, each module's and each prior's, added (each 0 when no count), - # are less than 2^27. + # 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 m in modules for p in _arr(_get(m, "priors")))) - rule(31, frames < 2 ** 27) + + 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(" 0": "rule 7 holds a contract with no module already: its callouts, of which there is one at least, each name a module below the modules' number, and none is below 0", - "7|bound|GtE for Gt in len(callouts) > 0": "rule 7 holds a contract with no callout already: each module's index is the module of a callout, and rule 6 gives one module at least", - "22|bound|GtE for Gt in len(stopped_in) > 0": "rule 18 holds a call's fault to a callout that is the extern of one of C's callouts already, so a call's fault with no such callout breaks rule 18", # 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", @@ -60,8 +56,10 @@ EQUIVALENT = { '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-28|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-28|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", + '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", } @@ -201,6 +199,9 @@ def python_mutants(path=SECOND): 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:]: @@ -255,7 +256,7 @@ def _replace(tree, old, new): # ------------------------------------------------------------------ the engine -_FUNCS = ("fn contract_form(", "pub fn problems(", "fn across(", "fn members(") +_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 = {">=": ">", ">": ">=", "<=": "<", "<": "<="} @@ -346,6 +347,9 @@ def _expr_mutants_bare(expr): recv, key = whole_get[: whole_get.rindex(".get(")], m.group(2) out.append(("null", "a null member read as absent: %s" % whole_get, expr[:m.start()] + "%s.get(%s).filter(|v| !v.is_null())" % (recv, key) + expr[m.end():])) out.append(("absent", "an absent member read as null: %s" % whole_get, expr[:m.start()] + "Some(%s.get(%s).unwrap_or(&Value::Null))" % (recv, key) + expr[m.end():])) + for m in re.finditer(r"\.(max|min)\(\)", expr): + other = "min" if m.group(1) == "max" else "max" + out.append(("extreme", "%s() for %s()" % (other, m.group(1)), expr[:m.start(1)] + other + expr[m.end(1):])) for m in re.finditer(r"(\w+)\.contains_key\(([^()]*)\)", expr): out.append(("null", "a null member read as absent: contains_key", expr[:m.start()] + "%s.get(%s).is_some_and(|v| !v.is_null())" % (m.group(1), m.group(2)) + expr[m.end():])) return out @@ -360,8 +364,8 @@ def rust_schemata(engine_rs, out_json): new = [] for no, line in enumerate(lines, 1): if not inside and any(line.startswith(f) or line.startswith(" " + f) or line.startswith("pub(crate) " + f) for f in _FUNCS): - # What across reads before rule 20's mark is the fault, which rules 20 to 28 share. - inside, depth, rule = True, 0, ("members" if "fn members(" in line else "20-28" if "fn across(" in line else "?") + # What across reads before rule 20's mark is the fault, which rules 20 to 35 share. + inside, depth, rule = True, 0, ("members" if "fn members(" in line else "20-35" if "fn across(" in line else "?") new.append(line) depth += line.count("{") - line.count("}") continue @@ -412,6 +416,10 @@ def _mutate_line(line, following, rule, no, mutants): 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) diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index 72725a2ed..83bc58aac 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -21739,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", diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index d0a1b258d..1b10e5b69 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -31947,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/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/main.rs b/wai/wai-quantum-cli/src/main.rs index a0e5340be..b4896904d 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -789,32 +789,39 @@ 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()))?; - if f.metadata().map_err(|e| format!("{}: {e}", p.display()))?.len() > MAX_JOB_BYTES { + let len = f.metadata().map_err(|e| format!("{}: {e}", p.display()))?.len(); + if len > most { return Ok(None); } - let mut b = Vec::new(); - f.take(MAX_JOB_BYTES + 1).read_to_end(&mut b).map_err(|e| format!("{}: {e}", p.display()))?; + // 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(MAX_JOB_BYTES + 1).read_to_end(&mut b).map_err(|e| format!("standard input: {e}"))?; + std::io::stdin().take(most + 1).read_to_end(&mut b).map_err(|e| format!("standard input: {e}"))?; b } }; - Ok((bytes.len() as u64 <= MAX_JOB_BYTES).then_some(bytes)) + Ok((bytes.len() as u64 <= most).then_some(bytes)) } fn read_text(file: Option<&Path>) -> Result { @@ -1499,15 +1506,9 @@ fn main() { // 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 {MAX_JOB_BYTES} bytes verify reads")); + die(&format!("the file holds more than the {} bytes verify reads", JOB_BOUND.bytes)); } - if as_json { - println!("{}", job::too_large(MAX_JOB_BYTES)); - } else { - println!("signature INVALID (too_large)"); - not_judged(job::TOO_LARGE, bundle.is_some()); - } - std::process::exit(1); + 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 @@ -1522,6 +1523,12 @@ fn main() { } let mut all = true; 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()); @@ -1531,6 +1538,9 @@ fn main() { } return; } + 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) => { @@ -1539,7 +1549,7 @@ fn main() { say_alone(&c, as_json, bundle.is_some()); } }; - let c = job::check_text(&src); + 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) { @@ -1551,7 +1561,7 @@ fn main() { if job::reported_alone(&c) { say_alone(&c, as_json, bundle.is_some()); } - let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}"))); + 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, 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 @@ -1594,7 +1604,7 @@ fn main() { } 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() }; + 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)); @@ -1757,6 +1767,17 @@ fn main() { /// 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()); @@ -1773,7 +1794,7 @@ pub(crate) fn not_judged(reason: &str, bundle: bool) { 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 1 GiB verify reads", + 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, }; diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 707d645e0..24864922d 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -620,7 +620,7 @@ 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. Every body @@ -993,19 +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; } - // Read with room around a job as deep as a reader takes (job::read_message). - let reply = match job::read_message(line.as_bytes()) { - 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) diff --git a/wai/wai-quantum-cli/src/wasmq.rs b/wai/wai-quantum-cli/src/wasmq.rs index 6b0efaaa7..d2c9ec755 100644 --- a/wai/wai-quantum-cli/src/wasmq.rs +++ b/wai/wai-quantum-cli/src/wasmq.rs @@ -84,7 +84,10 @@ pub struct PackArgs { 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")] diff --git a/wai/wai-quantum-cli/tests/verify.rs b/wai/wai-quantum-cli/tests/verify.rs index a657aa4a3..96ed49d0d 100644 --- a/wai/wai-quantum-cli/tests/verify.rs +++ b/wai/wai-quantum-cli/tests/verify.rs @@ -368,3 +368,108 @@ fn every_body_gets_its_verdict_from_verify() { } 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 index 587d90dc9..212c0b124 100644 --- a/wai/wai-quantum-cli/tests/wasmq.rs +++ b/wai/wai-quantum-cli/tests/wasmq.rs @@ -431,7 +431,7 @@ fn verify_gives_a_file_past_any_job_s_size_its_verdict_before_it_is_read() { } 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!(1u64 << 30), "{said}"); + 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()]); diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index da7f6d67f..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. A body that does not read as a job's, is no job, has no receipt, or is longer than the 64 MiB it reads (`too_large`) gets that reason as its verdict. An estimate or an algorithm's answer whose receipt holds is also run again | +| `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/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 87ef63a66..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, verify_body_bytes (what POST /v1/verify and /mcp read)." }, + "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,11 +282,11 @@ 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 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 (too_large, with read_at_most) 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\"} for an estimate; {\"valid\", \"reasons\", \"signed\", \"chain\", \"rerun\"} for an algorithm's answer); 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\"})", + "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, or an estimate's or an algorithm's request is not one the service runs again (the reason says which)" }, + "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" } @@ -344,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 ff3322711..3b39a0a39 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -98,15 +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 job of the largest runs (a -/// wasmQ job of 2^20 shots of many outcomes holds megabytes of counts). A longer one is -/// `too_large`, a verdict like any other (the provider protocol's section 8.4). -pub const VERIFY_BODY: usize = 64 << 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); @@ -207,6 +222,7 @@ async fn describe(State(app): State) -> Response { "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, @@ -254,6 +270,7 @@ pub(crate) fn bounds(app: &App) -> Value { "set_items": sets::MAX_ITEMS, "body_bytes": MAX_BODY, "verify_body_bytes": VERIFY_BODY, + "verify_body_values": VERIFY_VALUES, }) } @@ -575,44 +592,66 @@ async fn cancel(State(app): State, Path(id): Path) -> Response { async fn verify(State(app): State, ConnectInfo(peer): ConnectInfo, headers: HeaderMap, body: Result) -> Response { let bytes = match body { Ok(b) => b, - Err(e) if e.status() == StatusCode::PAYLOAD_TOO_LARGE => return reply(200, wai_quantum::engine::job::too_large(VERIFY_BODY as u64)), + 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}")), }; + // 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 { + 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, or its members already parsed. -pub(crate) enum Given<'a> { - Bytes(&'a [u8]), +/// 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. -pub(crate) async fn verdict(app: &App, who: &str, given: Given<'_>) -> Result { - use wai_quantum::engine::job; - 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::Members(v) => job::answer_of(&v), +/// 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 j = match answer { - job::Answer::Verdict(v) => return Ok(v), - job::Answer::Rerun(j) => j, - }; - if j["result"]["format"] == wai_quantum::engine::estimate::FORMAT { - crate::analysis::estimate_bounds(&j["request"])?; - } else { - crate::analysis::variational_bounds(j["result"]["call"].as_str().unwrap_or(""), &j["request"])?; + 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, } - crate::analysis::bounded(app, who, move || job::rerun(&j)).await } /// A design check ([`crate::analysis`]): the engine's answer, or the refusal. @@ -773,23 +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) -> Response { - let bytes = match body { +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) if e.status() == StatusCode::PAYLOAD_TOO_LARGE => { - return reply(413, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": format!("the message is longer than the {VERIFY_BODY} bytes this server reads; give a job past it to `wai-quantum verify`, which reads 1 GiB") } })); - } Err(e) => return reply(400, json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("the message could not be read: {e}") } })), }; - // Read with room around a job as deep as a reader takes (job::read_message), so that it - // reaches its verdict. - let msg = match wai_quantum::engine::job::read_message(&bytes) { - Ok(msg) => msg, + 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); @@ -833,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"); @@ -924,7 +995,7 @@ pub fn router(app: Shared) -> Router { .route("/v1/examples", get(examples)) .route("/v1/targets", get(targets)) .route("/v1/openapi.json", get(openapi)) - .route("/mcp", post(mcp).layer(DefaultBodyLimit::max(VERIFY_BODY)).get(mcp_get)) + .route("/mcp", post(mcp).get(mcp_get)) .route("/", get(index)) .fallback(|| async { error(404, "no such endpoint; GET /v1 lists them") }) .layer(axum::middleware::from_fn_with_state(Arc::clone(&app), guard)) diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index bfbb34aca..9b9376eea 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -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 }) @@ -807,7 +832,7 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu // 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::Bytes(text.as_bytes()), + 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(); @@ -816,18 +841,7 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu } }; match crate::http::verdict(app, who, given).await { - Ok(v) => { - 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 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) - } + Ok(v) => verdict_result(v), Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true), } } diff --git a/wai/wai-quantum-service/tests/verify.rs b/wai/wai-quantum-service/tests/verify.rs index 83cd8e787..5ad3c8ad8 100644 --- a/wai/wai-quantum-service/tests/verify.rs +++ b/wai/wai-quantum-service/tests/verify.rs @@ -219,14 +219,74 @@ fn one_answer_to_every_form_of_a_job() { 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); - // Past what verify reads: too_large, over HTTP; the tool's message past what the server - // reads is refused with the limit it states. + // 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(65 << 20)); + 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"] != json!(64 << 20) { - problems.push(format!("a job past 64 MiB: {code} {}", &answer[..answer.len().min(300)])); + 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(); diff --git a/wai/wai-quantum-views/src/lib.rs b/wai/wai-quantum-views/src/lib.rs index 2def1b65f..e63ca7f08 100644 --- a/wai/wai-quantum-views/src/lib.rs +++ b/wai/wai-quantum-views/src/lib.rs @@ -35,12 +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 { - // Read with room around a job as deep as a reader takes, so that it reaches its verdict. - let req: Value = match job::read_message(request.as_bytes()) { - 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") }), @@ -188,6 +190,17 @@ mod tests { 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] diff --git a/wai/wai-quantum/schemas/estimate.v1.json b/wai/wai-quantum/schemas/estimate.v1.json index ff54b54d7..1be749d02 100644 --- a/wai/wai-quantum/schemas/estimate.v1.json +++ b/wai/wai-quantum/schemas/estimate.v1.json @@ -390,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 189446a7d..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(), diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index 847c4b682..06f1e1958 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -113,16 +113,18 @@ fn of_a_set(job: &Value, c: &Check) -> Value { /// [`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 c = check_text(text); - if alone(&c) { - return Ok(verdict_of(&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)), } - let job: Value = serde_json::from_str(text).map_err(|e| format!("not JSON: {e}"))?; - if is_set(&job) { - return Ok(of_a_set(&job, &c)); - } - Ok(verdict_of(&c)) } /// A check as a surface gives it. @@ -221,19 +223,23 @@ 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; - let mut stack = vec![(v, 1usize)]; - while let Some((v, depth)) = stack.pop() { - match v { - Value::Array(a) => { - most = most.max(depth); - stack.extend(a.iter().map(|x| (x, depth + 1))); + 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())); } - Value::Object(o) => { - most = most.max(depth); - stack.extend(o.values().map(|x| (x, depth + 1))); + Some(Value::Object(o)) => { + most = most.max(stack.len()); + stack.push(Box::new(o.values())); + } + Some(_) => {} + None => { + stack.pop(); } - _ => {} } } most @@ -282,14 +288,235 @@ pub fn read_message(bytes: &[u8]) -> Result { } } -/// What a surface that reads at most `limit` bytes of a job finds of a longer one: -/// `too_large`, reported alone (§8.4). -pub fn too_large(limit: u64) -> Value { +/// 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!(limit); + 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 { @@ -306,27 +533,49 @@ 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 @@ -353,12 +602,10 @@ pub enum Answer { /// [`Answer`] of a job's text, read as §8.2 reads it. pub fn answer_of_text(text: &str) -> Answer { - let c = check_text(text); - if alone(&c) { - return Answer::Verdict(verdict_of(&c)); + match checked_text(text) { + (c, Some(job)) if !alone(&c) => answer_checked(job, &c), + (c, _) => Answer::Verdict(verdict_of(&c)), } - let Ok(job) = serde_json::from_str::(text) else { return Answer::Verdict(verdict_of(&c)) }; - answer_checked(job, &c) } /// [`Answer`] of a job given parsed: what a parsed job shows (the provider protocol's §9). diff --git a/wai/wai-quantum/src/engine/wasmq.rs b/wai/wai-quantum/src/engine/wasmq.rs index 2d19d5152..a7d18afd4 100644 --- a/wai/wai-quantum/src/engine/wasmq.rs +++ b/wai/wai-quantum/src/engine/wasmq.rs @@ -67,6 +67,19 @@ 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. @@ -297,7 +310,7 @@ pub fn problems(req: &Value, r: &Value) -> Vec { } } // Rule 12. - if count(&r["qubits"]).is_none_or(|q| q > u64::from(MAX_QUBITS)) { + 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"]) { @@ -458,16 +471,16 @@ pub fn problems(req: &Value, r: &Value) -> Vec { 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, r, &mut out); + across(c, req.get("wasmq").is_some(), r, &mut out); listed(out) } -/// Rules 20 to 31 (§6.5): what the writer guarantees across members, each a tie the +/// 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, r: &Value, out: &mut Vec) { +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); @@ -483,23 +496,43 @@ fn across(c: &Value, r: &Value, out: &mut Vec) { 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. + // (§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(mine * fault_fuel.unwrap_or(0))); + let theirs = count(&f["fuel"]).and_then(|all| all.checked_sub(e)); let max = count(&f["max"]); - if returned.zip(theirs).zip(max).is_none_or(|((k, used), max)| used < k || u128::from(used) > u128::from(k) * u128::from(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: each prior of each instance made whole was passed: a call of cabi_realloc and one - // of wasmq-prior, at least 1 each, and its bytes charged (§5.2, §5.3). + // 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 passed: u128 = mods.iter().flat_map(priors).map(|p| bytes(&p["bytes"]) + 2).fold(0, u128::saturating_add); - if count(&fuel["instances"]).is_none_or(|n| u128::from(n) < sets.saturating_mul(passed)) { - out.push(format!("rule 21: the instances' fuel, {}, is less than passing the priors to {sets} sets of instances takes", fuel["instances"])); + 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). @@ -519,9 +552,9 @@ fn across(c: &Value, r: &Value, out: &mut Vec) { 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'. - if fault_fuel.zip(count(&fuel["instances"])).is_none_or(|(n, all)| n > all) { - out.push(format!("rule 25: a fault after {} fuel outside a call, past the instances' {}", f["fuel"], fuel["instances"])); + // 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) { @@ -546,9 +579,10 @@ fn across(c: &Value, r: &Value, out: &mut Vec) { } } } - // 29: the bits of a shot are at most 2^20, and the shots' bits at most 2^28 (§4.8). + // 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.len() as u128).max() { + 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")); } @@ -561,15 +595,90 @@ fn across(c: &Value, r: &Value, out: &mut Vec) { 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 work = shots.saturating_mul(2 * n * n.div_ceil(32).max(1)).saturating_add(instances.saturating_mul(instance)).saturating_add(code).saturating_add(FUEL_WORK.saturating_mul(u128::from(budget("per_job")))); + 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 frames of the envelope (§2.3). - let frames = mods.iter().flat_map(|m| std::iter::once(&m["bytes"]).chain(priors(m).iter().map(|p| &p["bytes"]))).map(bytes).fold(bytes(&c["program"]["bytes"]), u128::saturating_add); - if frames >= MAX_ENVELOPE_BYTES as u128 { - out.push(format!("rule 31: {frames} bytes of program, modules and priors, past the frames an envelope of {MAX_ENVELOPE_BYTES} bytes holds")); + // 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 @@ -696,6 +805,7 @@ mod tests { 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"); @@ -741,11 +851,17 @@ mod tests { } } - /// A change to a kept job, and the problems of what it makes. + /// 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) } @@ -755,7 +871,33 @@ mod tests { r["fuel"]["budget"][name] = json!(v); } - /// Rules 20 to 31: each tie the writer keeps between members, broken alone in a job + /// 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. @@ -799,20 +941,65 @@ mod tests { }), ch(|_, r| { r["fuel"]["callouts"]["f"] = json!({ "calls": 3, "fuel": 50, "max": 23 }); })), - // 21: 32 shots of instances passed a prior of 87 bytes in less than 32 × 89 fuel. - (21, "cli-wasmq-run.json", "passing the priors to 32 sets", ch(|_, r| { - r["fuel"]["instances"] = json!(32 * 89 - 1); - r["fuel"]["used"] = json!(32 * 89 - 1 + 588348); + // 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"]["instances"] = json!(32 * 89); - r["fuel"]["used"] = json!(32 * 89 + 588348); + 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")), ch(|_, r| r["fault"]["reason"] = json!("stack_exhausted"))), + (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); @@ -823,15 +1010,27 @@ mod tests { 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!(2); - r["fuel"]["used"] = json!(7); + 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"); @@ -839,22 +1038,20 @@ mod tests { r["counts"] = json!({ "0": 2 }); r["callouts"] = json!(2); r["fuel"]["callouts"] = json!({ "f": { "calls": 2, "fuel": 2, "max": 1 } }); - r["fuel"]["used"] = json!(3); + // 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 } }); - r["fuel"]["used"] = json!(3); + // Two sets' inits and the stopped one. + r["fuel"]["instances"] = json!(3); + r["fuel"]["used"] = json!(5); r["fault"]["shot"] = json!(2); })), - // 27: the first shot's instances failing after a call. - (27, "cli-wasmq-fault-init.json", "after 1 callouts", ch(|_, r| { - r["callouts"] = json!(1); - r["fuel"]["callouts"] = json!({ "f": { "calls": 1, "fuel": 1, "max": 1 } }); - r["fuel"]["used"] = json!(2); - }), ch(|_, _| {})), // 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 }); @@ -899,7 +1096,7 @@ mod tests { })), // 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; + 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. @@ -912,11 +1109,44 @@ mod tests { r["shots"] = json!(1 << 10); r["qubits"] = json!(1 << 15); })), - // 31: a prior of 2^27 bytes beside a program and a module; then the most that fits. - (31, "cli-wasmq-fault-prior.json", "past the frames an envelope", ch(|c, _| c["modules"][0]["priors"][0]["bytes"] = json!(1 << 27)), ch(|c, _| { - let rest = c["program"]["bytes"].as_u64().unwrap() + 105; - c["modules"][0]["priors"][0]["bytes"] = json!((1u64 << 27) - rest - 1); + // 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); @@ -924,4 +1154,68 @@ mod tests { 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-wasmq/src/run.rs b/wai/wai-wasmq/src/run.rs index 8a8a22a18..737818485 100644 --- a/wai/wai-wasmq/src/run.rs +++ b/wai/wai-wasmq/src/run.rs @@ -224,9 +224,11 @@ pub fn run_recording(opened: &Opened, kind: EngineKind, key: &SigningKey, on_cal }) } +/// 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(b"wai:wasmq-transcript\x01"); + t.update(wai_quantum::engine::wasmq::TRANSCRIPT_PREFIX); t } diff --git a/wai/wai-wasmq/tests/vectors.rs b/wai/wai-wasmq/tests/vectors.rs index e891ba5ec..84b111cb5 100644 --- a/wai/wai-wasmq/tests/vectors.rs +++ b/wai/wai-wasmq/tests/vectors.rs @@ -466,3 +466,23 @@ fn each_refusal_is_refused_for_its_one_defect() { 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}"); +} -- 2.54.0 (Apple Git-157) From f431402da18c5201c2add05d4eb46049fe9b500d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 13:11:43 -0500 Subject: [PATCH 16/17] wasmQ: its names accounted for, its form built with the receipts, the modules' lockfile re-pinned - check-wai-quantum-names found 182 strings of wasmQ that nothing accounted for. The profile `wai.quantum.wasmq/1` is listed as a contract revision, the result `wai.quantum.wasmq.result/1` as a document with no schema file, and `wai.quantum.wasmq/2`, `wai.quantum.wasmq/1x` and `wai.quantum.wasmx` as strings the tests and the receipts corpus give a reader to see them refused. The registry export and REGISTRY.md are regenerated. - engine::wasmq reads job::same and receipt::canonical, which are compiled only with quantum_receipt, so the engine alone (`--no-default-features --features quantum_engine`) no longer built. The module is compiled with quantum_receipt, as job and receipt are; job::check, its one reader in the crate, is too. The wasmQ workflow now builds the engine with the receipts and without them, warnings denied. - modules.json pinned the modules' Cargo.lock as it was before wai-quantum 0.9.0. The merge of main that moved the lock changed one line, wai-quantum's version, and the modules rebuild on x86_64 Linux to the same bytes as their modules of record; only the lock's SHA-256 in modules.json moves. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-wasmq.yml | 12 +++++++++++ wai/REGISTRY.md | 5 +++++ .../wasmq/modules/modules.json | 2 +- wai/registry/capabilities.json | 20 +++++++++++++++++++ wai/wai-quantum/src/engine/mod.rs | 1 + wai/wai-rs/src/codecs/registry.rs | 5 +++++ 6 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wai-quantum-wasmq.yml b/.github/workflows/wai-quantum-wasmq.yml index 8e3a22ff9..7c1d3473f 100644 --- a/.github/workflows/wai-quantum-wasmq.yml +++ b/.github/workflows/wai-quantum-wasmq.yml @@ -97,6 +97,18 @@ jobs: 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 diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index d9c57b2e6..fffe0ef97 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -447,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`) | @@ -454,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 | @@ -464,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/quantum-conformance/wasmq/modules/modules.json b/wai/quantum-conformance/wasmq/modules/modules.json index 29c35e8c2..d7799ca06 100644 --- a/wai/quantum-conformance/wasmq/modules/modules.json +++ b/wai/quantum-conformance/wasmq/modules/modules.json @@ -19,7 +19,7 @@ "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": "8afe4fdcfcfe2de71e9f81376868686c67de2d21c82656e8a83dc827d60ba950", + "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", diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 1354409fa..6a393325f 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -3253,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/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index a5917d720..308da357c 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -255,5 +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-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index bbf996553..572b5edcd 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -1148,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"), @@ -1156,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`)"), @@ -1168,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. -- 2.54.0 (Apple Git-157) From 39fdb517aa433a24c6945aca895ecf2be0e9fd8a Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 13:15:20 -0500 Subject: [PATCH 17/17] wai: the reference package's test pattern named before its size, which the vanity check read as a count of tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "a 320 × 180 test pattern" (browser-sink §14.6's reference package and wai-web's compose/README.md, from main's 5df961d52) matched check-no-vanity-metrics.sh's " tests" and failed repository checks on main and on every branch that merges it. The same words with the size after the pattern say the same and match nothing. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/browser-sink.md | 2 +- wai/wai-web/compose/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 | -- 2.54.0 (Apple Git-157)