Skip to main content

WAI Extension: The Verifier Module

Mirrored from the canonical text at commit 0d3f0d7c ().

Status: Draft. Defines the verifier module: WAI’s verifiers as one WebAssembly module for wasm32-unknown-unknown, with an interface that needs no generated glue, versioned and pinned, so that any embedder (a page that checks records in its own tab, a console, a native host) runs the same checks with the same bytes and states which ones it ran. An embedder reserves the inputs of an operation in the module’s memory, calls the operation by its number, and reads one JSON answer. The operations are WAI’s verifiers of records defined elsewhere: OER/2 chains, wai.video.int_motion decode receipts, package receipts, receipt-ledger documents, exact-upscale cases, tone-mapping record sets, integer payload decodes, channel lineups and accounts’ audit chains (§4). This text defines the interface, the operations’ inputs and answers, the recipe that makes the module’s bytes a function of its sources, and the corpus every build is held to; it changes none of the records it verifies, and no core text. The reference is wai-verifier-module/, built from wai-rs’s verifiers, each the code its own corpus is checked with; its corpus is wai-verifier-module/corpus.json, and its second implementations are the corpora’s own independent verifiers, run by wai-verifier-module/crosscheck.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: WebAssembly 3.0, the specification build dated 2026-11-04 (Binary Format, Modules; Execution, memory.grow), as waism reads it; and the texts of the records of §4, at the revisions this repository holds.

1. Scope

In scope: the module and what it holds to (§2); its interface, the six exports, the inputs and their slots, the statuses and the answers (§3); the operations, each with its number, its inputs and its answer (§4); the recipe of a build and the module of record (§5); the corpus and how a build and the second implementations are held to it (§6); and the versions of the interface (§7).

Out of scope: what each record means and how it is verified, which its own text fixes (§4 names it); how an embedder obtains the module, and whom it trusts to have built it (an embedder holds the module to the digest of §5, or builds it by the recipe); and what an embedder does with an answer. The module does not hold a key, fetch anything, or keep anything between calls but the last answer.

Relationship to the other texts:

2. The module

The module is one WebAssembly 3.0 module. It:

Unlike a WAISM module it may contain floating-point instructions. Every floating-point instruction an operation reaches computes a result that WebAssembly 3.0 fixes (rounded to nearest, ties to even), or a NaN that no answer carries: the verifiers read a JSON number as its nearest binary64 and compare figures exactly. An embedder SHOULD run the module on an engine set to the NaN rule of WebAssembly 3.0’s deterministic profile (WAISM §7, nan-canonical); the corpus of §6 is run so.

A failed grow. Where memory.grow fails inside wai_call, the call traps and writes no answer. wai_input alone answers a failed reservation, with 0. A trap is no answer: the embedder states that the operation did not run, never that it accepted or refused.

3. The interface

3.1 Exports

exportkind and typewhat it does
memorymemorythe module’s one memory: the inputs and the answer
wai_verifier_abi() → (i32)the version of this interface, 1 (§7)
wai_input(i32, i32) → (i32)(slot, len): reserves input slot, from 0 to 15, of len bytes, for the next call, and returns its address; 0 where the slot is out of range or the memory cannot hold it
wai_call(i32) → (i32)(op): runs operation op over the inputs reserved since the last call, releases them, and returns its status (§3.3)
wai_output_ptr() → (i32)the address of the last call’s answer
wai_output_len() → (i32)the length of the last call’s answer, in bytes

3.2 Inputs and slots

An operation’s inputs are the slots reserved since the last call. They MUST be slots 0 upwards with none missing: inputs reserved in slots 0 and 2, and none in 1, are not an operation’s inputs, and the call answers unreadable with reason inputs. A slot reserved again before the call is replaced: its earlier region is released, and the later one is the input. An input of 0 bytes is an input; its address is not 0. The embedder writes each input’s bytes at the address wai_input returned before it calls wai_call, and nothing else into the memory.

Where an operation takes several files in one input (the priors of an upscale case, the envelopes and frames of a tone-mapping case, a parameter set of several files), the input is their file sequence, as WAISM §5 (The set) defines it: for each file, in ascending byte order of its name, the name’s length in one byte, the name, the file’s length in four bytes little-endian, and the file. Bytes that are not one are unreadable.

3.3 Statuses and answers

wai_call returns one of these, and the answer, at wai_output_ptr for wai_output_len bytes, is one JSON object, UTF-8, whose first member is outcome:

statusoutcomewhat it means
0acceptthe operation ran, and its record is accepted
1refusethe operation ran, and its record is refused: reason is the code its text gives
2unreadablethe operation did not run: its inputs are not what it reads; reason is inputs (their number or their slots, §3.2) or input_form (an input that is not the form the operation takes, with at, its slot)
3no_operationno operation of that number is in this module: unassigned, or not carried by this build (§4)

Status 2 is reserved to the carriage of an operation’s inputs: their number, a file sequence that does not read, a case document that is not JSON or not the case form of §4, and an OER/2 document that the document profile of oer-receipts §4 refuses whole, which gives no reasons to report. A record that reads as its carriage but is malformed by its own text is refused (status 1) with that text’s reason. The rest of an answer is the operation’s (§4). The answer stays where it is until the next call.

3.4 A call

An embedder instantiates the module with an empty import object, checks that wai_verifier_abi() returns a version it implements (§7), and then, for each verification, as many times as it likes on the one instance:

  1. for each input, from slot 0: wai_input(slot, len); a result of 0 is a refusal to reserve, and the embedder reports that the module could not hold the input; otherwise it writes the input at the address;
  2. status = wai_call(op);
  3. reads wai_output_len() bytes at wai_output_ptr(), a region that lies within the memory, and parses it as JSON.

A status other than 0 to 3, an answer that is not a JSON object with outcome first, an outcome that is not the status’s, or a region outside the memory, is no answer.

4. Operations

An operation’s number is fixed once assigned: no later version of the interface gives it to another operation. A build that does not carry an operation answers its number with status 3.

opnameinputswhat it verifies
0describe0 to 0nothing: the version of the interface (abi), and each operation this build carries (ops: its op, name and inputs, the least and the most it reads)
1oer.verify2 to 4an OER/2 chain against its keys (oer-receipts)
2int_motion.verify1 to 2a wai.video.int_motion decode receipt, and its re-derivation from its WIV1 payload (integer-payloads §15.1, §15.2)
3package.verify1 to 16a package receipt against its components’ stage indexes (packaging §9)
4ledger.verify1 to 1a receipt-ledger verification document (receipt-ledger §7.1, §7.2)
5upscale.verify1 to 2an exact-upscale case (exact-upscale §7.1, §7.2)
6tonemap.verify1 to 2a tone-mapping record set against what is held (exact-tonemap §8.1, §8.2)
7int_payload.decode2 to 3an integer payload decoded with its parameter set (integer-payloads): the digest of its canonical buffer
8channel.verify2 to 3a channel receipt, its lineup and a guide (channel-lineup §8.2)
9sha2561 to 1nothing: the SHA-256 of the input
10audit_chain.verify1 to 2an account’s audit export, against its keeper’s keys (audit-chain §7.1)
11audit_chain.append2 to 3an earlier audit checkpoint held to a later export (audit-chain §7.2)

Each operation’s inputs and answer:

5. The recipe and the module of record

A module’s identity is the SHA-256 of its bytes. Its bytes MUST depend on its source, its lockfile, the toolchain and this recipe, and on nothing else: not the host that builds it, its operating system or its architecture, the checkout’s path, the target directory or the user. The recipe (wai-verifier-module/build.sh):

  1. Rust 1.99.1, target wasm32-unknown-unknown, the crate’s library, cargo build --locked --release: one codegen unit, fat LTO, panic = "abort", no debug information, symbols stripped; the memory’s maximum given to the linker (§2).
  2. Every path of the building machine remapped: the checkout to /open-standards, the Cargo home to /cargo, the toolchain to /rustup and its library sources to /rustc/<commit>, the target directory to /target, and each build script’s output directory to /out/<package>-<version>.
  3. Each crate compiled for the target given, as -C metadata, a digest of what it is (its crate name, its package’s name and version, its crate types and its features) in place of the value Cargo derives, which takes in the compiler’s host and the absolute path of each path dependency. The compiler derives each crate’s identity, its symbols’ names and the order in which it emits them from that value; with Cargo’s own value, the same source built on two hosts gives two modules.
  4. No C compiled for the target: the C compiler, the C++ compiler and the archiver a build script would use for wasm32-unknown-unknown are false, so a crate that compiled C into the module would stop the build. No crate the operations need does, so no C compiler is an input.

wai-verifier-module/record/ holds the module of record, named by its SHA-256 in record/manifest.json, with the digest of the sources it was built from: for each of the repository’s crates it is built from, the reference’s own among them, its manifest, its build script and its sources, and every file those sources include by name; and the reference’s lockfile and recipe; each by its path and git object id. A build of those sources by the recipe MUST give the module of record’s bytes on every host; a build of later sources is held to the corpus (§6), not to record/’s bytes. The manifest names each host the module of record was built on.

6. The corpus

wai-verifier-module/corpus.json holds the cases a module is held to, each an operation, its inputs in their slots by length and SHA-256, the status its source states, what it states (in short), and the SHA-256 of the answer, byte for byte. Nothing in it is new data: every case is a vector of a record’s committed corpus, or a case made from committed files whose outcome its text states (a receipt sealed over a committed payload, then changed; a package receipt given another package’s index):

sourceopwhat
this text0, 9describe; sha256 of FIPS 180-2’s examples; an operation that is not one; too few and too many inputs; a slot missing; a slot reserved twice
oer-conformance/1every vector of its four vector files, and every document of unreadable/
wai-rs/tests/vectors/int_video/2receipts sealed over the two committed WIV1 payloads of keyframe 0, and receipts and payloads changed after sealing
pack-conformance/packs/3every package’s receipt with its indexes, and receipts given another package’s index, too few or too many indexes, or changed
live-conformance/ledger/4every vector, as its verification document
live-conformance/upscale/5every vector, with the priors it names
live-conformance/tonemap/6every case, with the envelopes and frames it holds
wai-int-module/corpus.json7every case of the decodes of §4
live-conformance/channels/8every verification vector, its edits applied
live-conformance/audit-chain/10, 11every export, against the keys it gives; every earlier checkpoint with its later export

Its generator, wai-verifier-cases, builds each case’s inputs from the committed files and answers it with the code the module runs, compiled for the generating host: an answer that is not what the case’s source states (the status, and every member it states, whole) stops it. Its check rebuilds the corpus’s own cases from their sources and requires the file byte for byte: a vector a source gains later joins the corpus when it is written again, and a vector changed or removed fails the check. A module is held to the corpus when, on one instance, call after call, and on an instance a case, every case gives the status and the answer the corpus states; the run digest, the SHA-256 of one line a case (its name, its status and its answer’s SHA-256), is then the corpus’s.

Second implementations. crosscheck.py answers every case with the record’s own independent verifier, which shares no code with wai-rs: oer2_verify.py, _int_motion_receipt.py, pack_verify.py, receipt_ledger_verify.py, upscale_verify.py, tonemap_verify.py, int_payload_verify.py, channel_lineup_verify.py and audit_chain_verify.py, and the interface’s rules (§3) applied from abi.json. It requires the status, the outcome, the reason and what the case states, and, for ops 4, 5, 6, 8, 10 and 11, the verifier’s whole answer to be what the case’s source states. Of op 7, it decodes a WIV1 payload of keyframe 0 and checks every refusal its parser gives; the digests of the other capabilities’ decodes are not decoded there, and are the ones the committed sidecars state. A mutated copy of the corpus (a status, a reason, a digest stated, an input’s digest changed) MUST fail it.

7. Versions

wai_verifier_abi returns the version of the interface the module implements. wai-verifier-module/abi.json holds each version’s entry: its exports, each with its kind and type, its slots, its statuses, and its operations with their inputs. A version’s entry, once published, never changes. Its digest, the SHA-256 of the entry written as JSON with its members sorted and no whitespace, is pinned here:

versiondigest of its entrypublished
1ecb8efc72f51a97bb0137b6a5d69e4544c32bbfed189c5510f5b5f08041cf27e2026-11-11

An export added, removed or given another type, a slot or a status changed, or an operation added, removed or changed, takes a new version: a new entry in abi.json, a new row here, and a module that returns it. An operation keeps its number and its name in every later version. An embedder MUST check the version before its first call, and MUST NOT call a module whose version it does not implement. The module’s check (wai-verifier-host abi, and crosscheck.py --abi beside it, written apart) requires a module’s exports to be exactly its version’s entry, the entry’s digest to be the one pinned here, the version to be the newest abi.json holds, and describe to list the version’s operations.

8. What an answer shows

An accepted answer shows that the record verified as its text verifies it, by this build of the reference verifier, on the inputs given: nothing about inputs not given (an int_motion.verify without the payload re-derives nothing, and says so with rederived: null), and nothing the record’s text leaves unchecked (an OER/2 figure is attested by its signature alone). That the answer came from the module, and not from a server that issued the records, is the embedder’s to show: it holds the module’s bytes to the module of record’s digest (§5), or builds them by the recipe, and runs it where the records’ issuer does not.

Appendix A. A reading

One host’s dated record, not a benchmark: the module of record’s size, and the corpus run on one instance under wasmtime 50.0.1 with Cranelift (wai-verifier-host bench), on the x86_64 Linux build host.

Measured 2026-11-12 on the x86_64 Linux build host: 32 cores and 122.8 GiB of memory, shared, its one-minute load 21 when the run began and 18 when it ended. The module of record, wai_verifier.wasm, is 4,249,549 bytes, SHA-256 cd8688e619b0294807f60a2fb0234c288b484407ca0f06fb7e7cdcabe7a6cccf; wasmtime 50.0.1 compiled it in 0.264 s. Five rounds of the corpus’s 778 cases (35,002,576 bytes of input) on one instance took 6.179, 6.003, 5.958, 5.783 and 5.977 s: at the median, 130.2 cases a second. In the fastest round:

opcasessecondscases a second
1 oer.verify1590.3376470.9
2 int_motion.verify130.0180721.4
3 package.verify180.001214,913.8
4 ledger.verify3214.995464.3
5 upscale.verify300.01701,766.4
6 tonemap.verify380.1354280.7
7 int_payload.decode810.2648305.9
8 channel.verify220.00474,654.2
9 sha25670.00183,838.0
10 audit_chain.verify710.004316,432.1
11 audit_chain.append150.00246,280.4

The rest of the corpus (describe and the interface’s cases that run no operation) took under a millisecond. A reading of one host on one day, not a benchmark: on the same host under a heavier load the day before, an earlier build ran the corpus of that day at 52.3 cases a second. A case’s time is its whole call: reserving and writing its inputs, the operation, and reading its answer. A ledger document carries its sources whole: the 321 ledger cases are 17,143,179 bytes of the input.