open-standards/wai/wai-ledger/smoke.sh
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

87 lines
4.9 KiB
Shell
Executable file

#!/usr/bin/env bash
# smoke.sh BASE_URL: the ledger service's promises, checked over HTTP against a running instance
# started with the corpus's seller key (CI starts one locally; see README.md).
#
# bash smoke.sh http://127.0.0.1:18251
#
# From the committed corpus (../live-conformance/ledger/vectors.json): it gives the service the
# first invoice's tariff and sources, asks for the day's invoice and requires the corpus's invoice
# byte for byte with the corpus's outcome; checks the invoice's verification document with the
# independent verifier, receipt_ledger_verify.py; requires a source billed already to be refused;
# and requires an altered document to be refused by POST /v1/verify. Each check prints one line and
# the script fails on the first that does not hold. Needs curl and python3.
set -euo pipefail
U=${1:?usage: smoke.sh BASE_URL}
U=${U%/}
HERE=$(cd "$(dirname "$0")" && pwd)
CORPUS="$HERE/../live-conformance/ledger/vectors.json"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
post() { curl -sS --max-time 300 -o "$3" -w '%{http_code}' -X POST "$U$1" -H 'content-type: application/json' --data-binary "@$2"; }
py() { python3 -I -c "$@"; }
curl -sS --max-time 30 "$U/v1" > "$tmp/describe.json"
py 'import json,sys
d=json.load(open(sys.argv[1])); c=json.load(open(sys.argv[2]))
assert d["service"]=="wai-ledger" and d["capability"]=="wai.delivery.ledger", d
assert d["seller_key"]==c["keys"]["seller"], "start the service with the corpus seller key (README.md)"
print("describe: ok, seller", d["seller_key"][:16]+"…")' "$tmp/describe.json" "$CORPUS"
# The tariff, then each source of the first invoice, in the corpus's order.
py 'import json,sys
c=json.load(open(sys.argv[1])); v=next(x for x in c["vectors"] if x["name"]=="a_day")
json.dump({"tariff": v["tariff"]}, open(sys.argv[2]+"/tariff.json","w"))
for k,n in enumerate(v["sources"]):
json.dump({"source": c["sources"][n]}, open(sys.argv[2]+"/source-%02d.json" % k,"w"))
print(len(v["sources"]))' "$CORPUS" "$tmp" > "$tmp/n"
code=$(post /v1/tariffs "$tmp/tariff.json" "$tmp/tariff.out")
[ "$code" = 201 ] || { echo "tariff: HTTP $code: $(cat "$tmp/tariff.out")"; exit 1; }
T=$(py 'import json,sys; print(json.load(open(sys.argv[1]))["tariff"])' "$tmp/tariff.out")
echo "tariff: ok, $T"
for f in "$tmp"/source-*.json; do
py 'import json,sys; d=json.load(open(sys.argv[1])); d["tariff"]=sys.argv[2]; json.dump(d, open(sys.argv[1],"w"))' "$f" "$T"
code=$(post /v1/sources "$f" "$f.out")
[ "$code" = 201 ] || { echo "source $(basename "$f"): HTTP $code: $(cat "$f.out")"; exit 1; }
done
echo "sources: ok, $(cat "$tmp/n") held"
# The day's invoice: the corpus's, byte for byte, with its outcome.
printf '{"tariff": "%s", "start": 1792108800000000, "end": 1792195200000000}' "$T" > "$tmp/issue.json"
code=$(post /v1/invoices "$tmp/issue.json" "$tmp/invoice.out")
[ "$code" = 201 ] || { echo "invoice: HTTP $code: $(cat "$tmp/invoice.out")"; exit 1; }
py 'import json,sys
c=json.load(open(sys.argv[1])); v=next(x for x in c["vectors"] if x["name"]=="a_day")
i=json.load(open(sys.argv[2]))
assert i["invoice"]==v["invoice"], "not the corpus invoice"
assert i["outcome"]==v["outcome"], "not the corpus outcome"
json.dump(i["document"], open(sys.argv[3],"w"))
print("invoice: ok,", len(i["outcome"]["lines"]), "lines, amount", i["outcome"]["amount"], i["outcome"]["currency"])' "$CORPUS" "$tmp/invoice.out" "$tmp/document.json"
# A buyer checks the document without the service.
python3 -I "$HERE/../live-conformance/receipt_ledger_verify.py" --document "$tmp/document.json" > "$tmp/indep.out"
echo "independent verifier: ok, $(py 'import json,sys; print(json.load(open(sys.argv[1]))["outcome"])' "$tmp/indep.out")"
# GET returns the same invoice and document.
D=$(py 'import json,sys; print(json.load(open(sys.argv[1]))["outcome"]["digest"])' "$tmp/invoice.out")
curl -sS --max-time 300 "$U/v1/invoices/$D" > "$tmp/get.out"
py 'import json,sys
a=json.load(open(sys.argv[1])); b=json.load(open(sys.argv[2]))
assert a["invoice"]==b["invoice"] and a["document"]==b["document"] and b["outcome"]["outcome"]=="accept"
print("get invoice: ok")' "$tmp/invoice.out" "$tmp/get.out"
# A source billed already is refused, with its reason.
code=$(post /v1/sources "$tmp/source-04.json" "$tmp/again.out")
py 'import json,sys
d=json.load(open(sys.argv[1])); assert sys.argv[2]=="422" and d["reason"]=="record_duplicate", (sys.argv[2], d)
print("billed again: refused", d["reason"])' "$tmp/again.out" "$code"
# A document whose invoice was altered is refused by POST /v1/verify.
py 'import json,sys
d=json.load(open(sys.argv[1])); b=bytearray(bytes.fromhex(d["invoice"])); b[-1]^=1; d["invoice"]=b.hex()
json.dump(d, open(sys.argv[2],"w"))' "$tmp/document.json" "$tmp/tampered.json"
post /v1/verify "$tmp/tampered.json" "$tmp/tampered.out" > /dev/null
py 'import json,sys
d=json.load(open(sys.argv[1])); assert d["outcome"]=="refuse" and d["reason"]=="bad_signature", d
print("tampered document: refused", d["reason"], "at", d["at"])' "$tmp/tampered.out"
echo "smoke: all checks hold"