open-standards/wai/wai-ledger/README.md
Transaction Science cc087bde44 wai-ledger: the receipt ledger as a service; registry, SPEC §5, CHANGELOG and CI
wai/wai-ledger: tariffs, sources and invoices kept in a directory the service
owns (one process holds its lock; every file written whole; the state read back
and verified again on start); POST /v1/tariffs, /v1/sources, /v1/invoices and
/v1/verify, GET /v1/invoices/{digest} with the invoice's verification document.
Its tests issue the corpus's invoices byte for byte and recover the chain after
a restart; smoke.sh checks it over HTTP, the document checked by
receipt_ledger_verify.py --document.

wai.delivery.ledger registered with WLT1 and WLI1 (registry export
regenerated); unit tests of the module; the CI step beside settlement's and
.github/workflows/wai-ledger.yml.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:41:30 -04:00

84 lines
4.7 KiB
Markdown

# wai-ledger
The WAI receipt ledger ([`extensions/receipt-ledger.md`](../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:
```sh
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
```sh
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:
```text
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
```sh
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)
```