#!/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"