open-standards/wai/wai-ledger
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 3b3cedfc21 wai-quantum 0.7.0, wai-quantum-cli 0.2.0, wai-quantum-qpu 0.3.0 and the job service 0.2.0: the release's versions, its changelog heading, and every lock
The engine, the command line, the provider crate and the job service move to
their next 0.y, which is a major bump before 1.0 (VERSIONING.md); the Python
package stays 0.1.0 and the editor extension keeps its version. Every
dependent's requirement names the new version, and the sixteen lockfiles that
resolve any of them move with them, each read with --locked.

cargo-semver-checks keeps its baselines at the versions last published (0.6.16,
0.1.4 and 0.2.0); wai-quantum-qpu joins the check. Against them it lists the
breaking changes and passes, as each major is bumped.

The changelog's release heading summarises the twelve entries below it and
lists what a caller may have to change. DEPRECATIONS.md stays empty.

The engine's version is among the sealed bytes of a stamped result, so the
page's pinned digests move: the compile's 0a165415... to ac948133..., the
estimate's 0cd25cf6... to 8715e76b... The job pin strips the engine and
schema and does not move. The notebooks' stored outputs are re-stored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:44:28 -04:00
..
src wai: receipt ledger — streams stitched by others, from stitched-presentation's records 2026-10-25 22:13:26 -04:00
Cargo.lock wai-quantum 0.7.0, wai-quantum-cli 0.2.0, wai-quantum-qpu 0.3.0 and the job service 0.2.0: the release's versions, its changelog heading, and every lock 2026-10-27 23:44:28 -04:00
Cargo.toml wai-ledger: the receipt ledger as a service; registry, SPEC §5, CHANGELOG and CI 2026-10-25 21:41:30 -04:00
README.md wai-ledger: the receipt ledger as a service; registry, SPEC §5, CHANGELOG and CI 2026-10-25 21:41:30 -04:00
smoke.sh wai-ledger: the receipt ledger as a service; registry, SPEC §5, CHANGELOG and CI 2026-10-25 21:41:30 -04:00

wai-ledger

The WAI receipt ledger (extensions/receipt-ledger.md) as a service. A seller and a buyer both sign a tariff. The ledger verifies each record it is given by the extension that defines it: a room's session receipt, a delivering party's settlement statement with the acknowledgements its downstream parties signed, a package receipt with its indexes, an ad room's receipts with the players' presentation claims. For a period, it signs the invoice whose every line cites the records it came from, with the joules those records state per scope and per acquisition class, and two reports: the ad decisions against the presentation claims, and the delivery of each party with the bytes two parties were acknowledged for twice.

Every invoice is served with its verification document: the tariff, the invoice, the invoice before it in its chain, and the sources. Any verifier of the extension checks it without this service, the corpus's second implementation among them:

python3 -I ../live-conformance/receipt_ledger_verify.py --document document.json

Payments and tax are not here: an invoice is the figures a payment rests on.

Running it

cargo build --locked --release
WAI_LEDGER_DIR=/path/to/ledger WAI_LEDGER_KEY=/path/to/seller.key WAI_LEDGER_ADDR=127.0.0.1:8251 \
  ./target/release/wai-ledger serve
variable meaning
WAI_LEDGER_DIR the directory the service owns (required); created when absent
WAI_LEDGER_KEY the seller key's file: 32 bytes, the Ed25519 seed; made with mode 0600 when absent
WAI_LEDGER_ADDR the address it listens on (default 127.0.0.1:8251)

wai-ledger key prints the seller key of WAI_LEDGER_KEY, which a buyer's tariff names as its seller. wai-ledger verify FILE prints a verification document's outcome.

The directory:

LOCK                                  held exclusively while a process serves the directory
tariffs/<digest>/tariff.wlt           the tariff, both signatures
tariffs/<digest>/sources/<n>.json     the sources held, in the order they arrived
tariffs/<digest>/invoices/<n>.wli     the chain, in order
tariffs/<digest>/invoices/<n>.json    each invoice's verification document

Every file is written whole: to a temporary name, synced, renamed into place, and its directory synced. On start the service takes the lock (a second process on the same directory is refused), then reads every tariff, source and invoice back and verifies each again, and refuses to start on a chain that does not link.

Endpoints

GET /v1 the service, its seller key and its limits
GET /v1/health ok
POST /v1/tariffs {"tariff": <hex>}: a tariff whose seller is the service's key, both signatures verified; 201 and its digest
GET /v1/tariffs/{digest} the tariff, its chain of invoices, and the sources held and not yet billed
POST /v1/sources {"tariff": <digest>, "source": {...}}: the source verified by its extension, in the tariff's scope, under no other tariff, within the tariff's period, and checked against every source held (record_duplicate, record_overlap) and every record billed (record_rebilled); 201 and the records it holds
POST /v1/invoices {"tariff": <digest>, "start": <µs>, "end": <µs>}: the invoice of the period over the sources held, not yet billed, that lie in it (a package always does), chained after the last, signed, verified and kept; 201, the invoice, its outcome with the reports, and its verification document
GET /v1/invoices/{digest} the invoice, its outcome and its verification document
POST /v1/verify a verification document: its outcome

A refusal is 422 with {"outcome": "refuse", "reason", "at"}, the extension's reason and place; a source refused by its own extension names that extension's reason and its place after "source". A body that is not what the endpoint takes is 400; an unknown tariff or invoice is 404. A request body is at most 64 MiB. Requests that change the ledger run one at a time; POST /v1/verify and GET /v1/invoices/{digest} verify at most two documents at a time.

The service has no accounts: a tariff binds a buyer by the buyer's own signature, and a source is accepted only when it verifies and the tariff's scope names its party, whoever submits it.

Checks

cargo test --locked --release          # the corpus's invoices issued byte for byte, the chain
                                       # recovered after a restart, each source billed once,
                                       # one process per directory
bash smoke.sh http://127.0.0.1:18251   # over HTTP, against an instance started with the corpus's
                                       # seller key (see .github/workflows/wai-ledger.yml)