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]>
84 lines
4.7 KiB
Markdown
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)
|
|
```
|