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_motiondecode 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 iswai-verifier-module/, built fromwai-rs’s verifiers, each the code its own corpus is checked with; its corpus iswai-verifier-module/corpus.json, and its second implementations are the corpora’s own independent verifiers, run bywai-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:
- waism. WAISM is the profile of decoder modules: their six exports (§5 there), the integer and the float level, and a sink’s checks. The verifier module is not a decoder module: it exports another interface, and it holds floating point (the records it reads are JSON, whose numbers OER/2’s document profile reads as binary64), so it meets neither level. It keeps WAISM’s rules on what a module may be where they apply (§2), and its file sequence (§5 there, The set) is the form in which an operation takes several files (§3.2).
- The records of §4. An operation’s answer is the verdict of the reference verifier of that record, written as that record’s corpus states its outcomes: a module that answers otherwise than the record’s text is wrong, and the record’s text wins.
2. The module
The module is one WebAssembly 3.0 module. It:
- is at most 16,777,216 bytes long, as WAISM’s R1;
- is valid and uses no construct outside WebAssembly 3.0, no shared memory and no atomic instruction, as WAISM’s R2;
- contains no relaxed vector instruction, as WAISM’s R4;
- imports nothing (WAISM’s R5): its answers are a function of its inputs alone;
- defines exactly one memory, of address type
i32, with a declared maximum of at most 65,536 pages (WAISM’s R6); the module of record declares 16,384 pages, 1 GiB; - defines at most one table, of
funcref, whose minimum equals its maximum, and contains notable.grow,table.fillortable.set(WAISM’s R7); - has no start section (WAISM’s R8);
- exports exactly the six exports of §3.1 and nothing else.
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
| export | kind and type | what it does |
|---|---|---|
memory | memory | the 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:
| status | outcome | what it means |
|---|---|---|
| 0 | accept | the operation ran, and its record is accepted |
| 1 | refuse | the operation ran, and its record is refused: reason is the code its text gives |
| 2 | unreadable | the 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) |
| 3 | no_operation | no 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:
- 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; status = wai_call(op);- reads
wai_output_len()bytes atwai_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.
| op | name | inputs | what it verifies |
|---|---|---|---|
| 0 | describe | 0 to 0 | nothing: 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) |
| 1 | oer.verify | 2 to 4 | an OER/2 chain against its keys (oer-receipts) |
| 2 | int_motion.verify | 1 to 2 | a wai.video.int_motion decode receipt, and its re-derivation from its WIV1 payload (integer-payloads §15.1, §15.2) |
| 3 | package.verify | 1 to 16 | a package receipt against its components’ stage indexes (packaging §9) |
| 4 | ledger.verify | 1 to 1 | a receipt-ledger verification document (receipt-ledger §7.1, §7.2) |
| 5 | upscale.verify | 1 to 2 | an exact-upscale case (exact-upscale §7.1, §7.2) |
| 6 | tonemap.verify | 1 to 2 | a tone-mapping record set against what is held (exact-tonemap §8.1, §8.2) |
| 7 | int_payload.decode | 2 to 3 | an integer payload decoded with its parameter set (integer-payloads): the digest of its canonical buffer |
| 8 | channel.verify | 2 to 3 | a channel receipt, its lineup and a guide (channel-lineup §8.2) |
| 9 | sha256 | 1 to 1 | nothing: the SHA-256 of the input |
| 10 | audit_chain.verify | 1 to 2 | an account’s audit export, against its keeper’s keys (audit-chain §7.1) |
| 11 | audit_chain.append | 2 to 3 | an earlier audit checkpoint held to a later export (audit-chain §7.2) |
Each operation’s inputs and answer:
oer.verify. Slot 0, the chain: a JSON array of receipts. Slot 1, the keys: the object of key id to public key a vector file publishes. Slot 2, where given, the records the receipts were served from (an object;nullfor none, so that slot 3 can be given alone); slot 3, the meter’s ledger. Each input is read under oer-receipts §4’s document profile, and one it refuses whole isunreadable. The answer’sreasonsare the reason names the chain gives (§7 there, each by its name before any:), sorted, withrecord_mismatchwhere a receipt served from a record does not rehash to it andledger_mismatchwhere the meter does not balance;receiptslists each failing receipt id with its reasons in full,unmeteredthe ids of the receipts that carry the unmetered marker (§3.1 there), andcountthe receipts. Accepted whenreasonsis empty; refused otherwise, with the first reason asreason.int_motion.verify. Slot 0, the receipt’s JSON form; slot 1, where given, itsWIV1payload. Refusedreceipt_unreadablewhere §15.2 there does not read the text,receipt_inconsistentwhere §15.1’s consistency fails, andreceipt_not_rederivedwhere the payload does not re-derive the receipt. Accepted, the answer names itsreceipt_hash,codestream_hash,clip_hash, geometry, frames and figure, andrederived(true, ornullwhere no payload was given).package.verify. Slot 0, the receipt; slots 1 onwards, its components’ stage indexes in the receipt’s component order. A version-1 receipt names no index and is refusedreceipt-version. Refused with the first code §9 there gives; accepted, the answer names the receipt hash, the asset, each component’s index digest and stage root, and the counts.ledger.verify. Slot 0, the verification document of §7.1 there. The answer is the outcome of §7.2 there in the form the ledger corpus states it (live-conformance/ledger/).upscale.verify. Slot 0, the case: the members of §7.1 there as a vector of the upscale corpus (live-conformance/upscale/vectors.json) writes them, withoutname,aboutand the outcome:policy,claims,upscales,presentations,objects,decoded,priors(each by its name),residualsandtargets. Slot 1, the priors it names, their file sequence under those names. A prior named and not given isunreadable. The answer is the outcome in the corpus’s form:statementswhere accepted,reasonandatwhere refused.tonemap.verify. Slot 0, the case:records(the records’ texts),policy(require-prooforaccept-unrecomputed),envelopes(the names of the envelopes held) andframes(for each decoded clip held: its envelope’sobjectcontent hash,width,height,countandfile, the frames’ RGB8 one after another). Slot 1, the files those name, their file sequence. The answer is the outcome in the tone-mapping corpus’s form (live-conformance/tonemap/).int_payload.decode. Slot 0, the capability’s term; slot 1, the payload; slot 2, its parameter set, as WAISM §5 passes a set (one file’s bytes, or the file sequence of several), omitted for a capability that reads none. The capabilities are those ofwai.video.int_motion,wai.video.int_hyper,wai.neural.int_hyper,wai.audio.int_codec,wai.splat.int_codec,wai.neural.int_synthandwai.neural.int_mlicpp. Accepted, the answer’ssha256is the SHA-256 of the decode’s canonical buffer (determinism-tiers §2: for video the frames’ SHA-256s one after another, so it is the clip digest the integer-payload corpus states), with itsbytes; refused, the capability’s own refusal code.channel.verify. Slot 0, the channel receipt’s JSON carriage; slot 1, the lineup (WLU1); slot 2, where given, a guide. The answer carries the verification’sfactsin the form the channel corpus states them (§8.3 there); refused with the verdict’s code, andat, the part.sha256. The answer’ssha256andbytes.audit_chain.verify. Slot 0, the audit export’s JSON Lines; slot 1, where given, the keys the verifier holds for the keeper, a JSON array of public keys in hexadecimal, ornull(none given: the signer is reported, not checked). The answer is the outcome in the audit corpus’s form (live-conformance/audit-chain/): the account, the anchor, the entries, and the checkpoint’sseq,head, digest and signer where accepted;reasonandatwhere refused.audit_chain.append. Slot 0, the earlier checkpoint; slot 1, the later export; slot 2, the keys, as above. Accepted where the later export only appended to the chain the earlier checkpoint closed, withfromandto, the two checkpoints’ sequence numbers (the corpus’sappended); refused with its reason andatotherwise.
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):
- 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). - Every path of the building machine remapped: the checkout to
/open-standards, the Cargo home to/cargo, the toolchain to/rustupand its library sources to/rustc/<commit>, the target directory to/target, and each build script’s output directory to/out/<package>-<version>. - 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. - No C compiled for the target: the C compiler, the C++ compiler and the archiver a build
script would use for
wasm32-unknown-unknownarefalse, 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):
| source | op | what |
|---|---|---|
| this text | 0, 9 | describe; 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/ | 1 | every vector of its four vector files, and every document of unreadable/ |
wai-rs/tests/vectors/int_video/ | 2 | receipts sealed over the two committed WIV1 payloads of keyframe 0, and receipts and payloads changed after sealing |
pack-conformance/packs/ | 3 | every package’s receipt with its indexes, and receipts given another package’s index, too few or too many indexes, or changed |
live-conformance/ledger/ | 4 | every vector, as its verification document |
live-conformance/upscale/ | 5 | every vector, with the priors it names |
live-conformance/tonemap/ | 6 | every case, with the envelopes and frames it holds |
wai-int-module/corpus.json | 7 | every case of the decodes of §4 |
live-conformance/channels/ | 8 | every verification vector, its edits applied |
live-conformance/audit-chain/ | 10, 11 | every 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:
| version | digest of its entry | published |
|---|---|---|
| 1 | ecb8efc72f51a97bb0137b6a5d69e4544c32bbfed189c5510f5b5f08041cf27e | 2026-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:
| op | cases | seconds | cases a second |
|---|---|---|---|
1 oer.verify | 159 | 0.3376 | 470.9 |
2 int_motion.verify | 13 | 0.0180 | 721.4 |
3 package.verify | 18 | 0.0012 | 14,913.8 |
4 ledger.verify | 321 | 4.9954 | 64.3 |
5 upscale.verify | 30 | 0.0170 | 1,766.4 |
6 tonemap.verify | 38 | 0.1354 | 280.7 |
7 int_payload.decode | 81 | 0.2648 | 305.9 |
8 channel.verify | 22 | 0.0047 | 4,654.2 |
9 sha256 | 7 | 0.0018 | 3,838.0 |
10 audit_chain.verify | 71 | 0.0043 | 16,432.1 |
11 audit_chain.append | 15 | 0.0024 | 6,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.