8. Sandboxed functions
For: a builder whose logic must run inside a sale and give the same answer on the server and on an offline till. Reference implementation: the kit's README and contract (the commands below are the README's) · the first measurements are summarised in the repository's README.
A sandboxed function is a small program you write in Rust and compile to WebAssembly. The shop would run the same file on its server and on the till, so a decision gives the same answer online and offline. This chapter shows how to build one and check it yourself with the kit.
Read this first
- There is nowhere to install a function today. No published extension point offers a sandboxed function yet. The contract lists four extension points that would take one; none has shipped.
- The kit has been tried only on our own test hosts, not the product's. Everything below ran on the programme's server host and a browser host (run in Node), because the product's host does not exist yet.
- A function cannot change a price today. Nothing applies a function's answer to a sale yet.
When to use it
Rung 5 of the ladder (chapter 3), the last before asking for an extension point (chapter 9): only when a decision must be computed by your own logic and must also work on a till with no network. Try configuration (chapter 4) and a till key (chapter 6) first. Write it in Rust, the language measured. A function that only ever runs on the server may also be compiled from JavaScript (see What is refused).
Steps
-
Install what the kit needs.
- Rust, installed with rustup at its default place (the kit looks for
~/.cargo/bin/cargo), with thewasm32-unknown-unknowntarget. - Node 22 or later.
- Python 3.10 or later with the one library the README names,
wasmtime==49.0.0.
Then build the two meters once, each with
cargo build --release: the fuel meter intools/wasm_kit/meterand the stack meter intools/wasm_kit/stackmeter. The kit looks for them at those paths, and publishing fails without them. Run every command from the repository's root. (The two test hosts the kit uses are intools/wasm_kit/vendor/andtools/wasm_kit/hosts/.) - Rust, installed with rustup at its default place (the kit looks for
-
Copy the template and write your function. Edit one file only,
src/point.rs. It ships with a discount adjuster as the worked example.cp -r tools/wasm_kit/template my_point -
Write to the contract.
- One message in, one answer out, both UTF-8 JSON.
- Numbers are whole numbers. Money is in integer minor units, quantity in integer thousandths (
qty_milli). No fractions, signs or exponents in numbers; nesting at most 32. - Check the input in the published order. On the first bad field, answer
{"error":"invalid_input","at":"<first failing path>"}. - The function sees only what the host gives it:
pp.read_input,pp.write_output,pp.log, an exportedrun()and its memory. A clock, randomness, the network or a start function is refused when the module loads. - Limits: fuel 11 000 000 units; memory 10 MiB; an answer at most 512 KiB; the log 1 KiB; a fresh instance on every call.
- Keep the template's own JSON code. It uses whole numbers only, on purpose; a common JSON library brings in floating-point instructions, which a till function may not have.
-
Build. This runs your Rust tests first. Give the folder of your crate, relative or absolute.
python3 tools/wasm_kit/kit.py build my_pointThe raw module lands in
my_point/target/wasm32-unknown-unknown/release/, named after your crate. -
Publish. Scan, stack meter, fuel meter, memory cap, size cap and load checks, in that order; it writes a
*.publish.jsonwith the size, sha256 and bounds.python3 tools/wasm_kit/kit.py publish <raw.wasm> <out.wasm>A till function may have no floating-point instruction, no SIMD, no atomics, keeps recursion shallow (the stack bound is 10 000 units, written into the module), and is at most 256 KB once metered. A function that never runs on a till has a shorter path, in step 7.
-
Prove the same answer on both hosts, on samples of your own in the shape of the template's
samples/samples.json(an input and the expected answer;must_answermarks the realistic ones, the rest must at least agree). A stress basket may run out of fuel, but it must do so identically on both hosts:python3 tools/wasm_kit/kit.py parity <out.wasm> my_point/samples/samples.json --metered -
Run the local admission. It takes your raw build, publishes it itself and runs every sample on both hosts. A trap on a realistic sample, a split between the hosts or a wrong answer is a refusal, named. A module that already carries a meter is refused, because a module's own counter is never trusted:
python3 tools/wasm_kit/kit.py admit <raw.wasm> my_point/samples/samples.jsonA function that never runs on a till skips steps 5 and 6 and runs the same command with
--server-onlyadded. The module is then checked as given, with no size cap, and run on the server host only: no browser host, no parity, no kit meter (the server host's own fuel limit still applies), and floats and SIMD are allowed, atomics never.This is the check you run yourself. It does not certify your module; what we run on a submission, and the signing of a module's sha256 that is planned for it, are not covered by the kit yet (chapter 17).
What is refused, and why
- Anything the module asks for beyond the three host functions (a clock, randomness, the network): refused at load, named, so the same input gives the same answer everywhere. Likewise a missing memory maximum or one above 10 MiB, and an unusual shape such as a start function.
- A floating-point, SIMD or atomic instruction in a till function, because a float's result can differ by host.
- A module that already carries a meter.
- A run past its fuel, a pointer outside the module's memory, an answer over 512 KiB: a trap at the call, named, never half an answer.
- JavaScript on the till. On the till it is Rust only: a JavaScript function started too slowly on our slow till stand-in. A server-only function in JavaScript is admitted, with no size cap. How to build that JavaScript module is not in the kit's README.
When your service is down, and when the till is offline
- There is no service. The function runs inside the shop and inside the till; nothing calls you.
- Offline, the till would run the same bytes from its offline copy. No till has done it yet. Carrying your module in the till's offline copy belongs to the pricing extension point still to come.
- A refusal, a trap or an over-budget run means no adjustment, never half of one.
Technical notes
What the kit has proven
- The template (35 707 bytes raw, 78 688 published, no float, SIMD or atomic instruction): 192 of 192 samples identical on the server host and the browser host. The 181 it answered all equal the reference answers; the 11 oversized stress baskets stopped for fuel on both hosts alike. Admitted.
- Twenty-five hostile modules were all refused by the local admission, as a till function and as a server-only one, each with a named reason.
- The stack bound: a deliberately recursive module, run through the kit's meters, answered at depth 1 000 and trapped from 1 500 on both hosts alike.
- Speed on a slow till stand-in (the earlier spike, a browser slowed six times): a Rust function started in 11.7 ms plain and 21.3 ms metered, against 50 ms.
- The kit's own tests, 5 of 5.
What it has not
- Against the product. No extension point has shipped, so nothing ran on the product's own host.
- Chrome and a real till through the kit. The kit's browser host runs in Node; the earlier spike ran Chrome. A real low-end Android till's timing is still owed.
- The stack bound for any function shape but that one: it counts frames and ignores deeper operand stacks and spills.
- A function changing a price, and a function's answer being applied at all.
- JavaScript on the server beyond the samples. Languages other than Rust and JavaScript were not measured.
- The offline copy carrying your module on a real till, and any intake check of a submission.
Why it is built this way
- The template's integer-only JSON. The template carries its own integer-only JSON on purpose: a common JSON library put 26 floating-point instructions into the earlier spike's function.
- The size cap on
publish.publish --server-onlyapplies no size cap, asadmit --server-onlydoes; until 2026-10-07 it still applied the till's 256 KB cap (fixed with a test, kit PR for row 10k). A function that runs on a till keeps the cap. - JavaScript on the till. JavaScript compiled with Javy gave identical answers but took 68.8 ms to start in a browser slowed six times, our till stand-in, against a 50 ms budget. The older recipe says JavaScript is "not offered today". The kit's README, later, says a server-only function in JavaScript is admitted with no size cap: the Javy build was admitted through
admit --server-only, and five hostile JavaScript modules were still refused. - Offline. In the first spike the same answers came back from a browser with its network switched off; no till did it.
See also
3. The ladder and the four rules · 6. Till keys · 9. The extension-point catalogue and requests · 14. Testing and release · 17. Submitting, support, feedback