Appendices · Chapter 41
Appendix D — Lab Manual & Autograder {.unnumbered}
Appendix D — Lab Manual & Autograder
Open this appendix when something breaks. It covers environment setup, lab directory layout, autograder mechanics, submission requirements by tier, and common failure modes with fixes.
The labs are not optional elaborations on the chapters. They are the proof of understanding. A reader who can explain RFC 8785 but cannot produce a byte- identical canonical form has not understood RFC 8785.
D.1 — Environment Setup
Install in this order. Each dependency is listed with the command to verify it is present and at a sufficient version.
Python 3.10 or later
python --version
# Must print Python 3.10.x or higher
Most labs require 3.10+ (PEP 604 union types). If your system Python is older, use pyenv or a virtual environment with a newer interpreter.
Install the canon package in editable mode
Run this from the repository root (~/litigation-db):
pip install -e ".[test]"
This installs meridian-canon in editable mode, plus pytest and all test dependencies. After this, import meridian.canon works from any directory, and pytest discovers all test suites.
Go 1.21 or later (required for Lab 25 only)
go version
# Must print go1.21.x or higher
Install via the official Go installer at https://go.dev/dl/ or on macOS:
brew install go
PostgreSQL 16 or later (required for database-touching labs)
psql --version
# Must print psql (PostgreSQL) 16.x or higher
On macOS:
brew install postgresql@16
brew services start postgresql@16
pgvector (required for Labs 9–10)
With Postgres running, connect and install the extension:
CREATE EXTENSION IF NOT EXISTS vector;
The extension must be installed in the database the lab connects to. If you
get ERROR: extension "vector" is not available, install the package first:
brew install pgvector
pnpm (required for any JavaScript labs)
pnpm --version
# Must print 8.x or higher
Install: npm install -g pnpm or brew install pnpm. --- ## D.2 — Repository Layout The lab materials live under docs/textbook/labs/. Each lab has its own directory named chNN_descriptive_name/.
docs/textbook/
EDITORIAL.md
appendices/
B_worked_attestation.md ← this book's Appendix B
D_lab_manual.md ← this file
labs/
ch05_hashing/
README.md
student/ ← skeleton code; student writes here
reference/ ← instructor solution (gitignored at release)
test_lab.py ← autograder
vectors.txt ← test vectors
ch06_signatures/
ch07_canonicalization/ ← RFC 8785; sourced from cyberphone vectors
README.md
student/
canonicalize.py ← student implements this
forge.json ← student writes (Problem 7.6)
findings.md ← student writes (Problem 7.6)
reference/
test_lab.py
vectors/
p1_basic.json
p2_high_codepoints.json
p3_numbers.json
p3_canonicalize.json
p4_surrogates.json
p5_numeric_edges.json
cyberphone_subset.json
ch08_schemas/
ch09_embeddings/
ch10_hybrid_retrieval/
ch11_llm_extraction/
ch12_adversarial/
ch13_pipeline/
...
ch25_verifier/ ← cross-language verifier; Go + Python
README.md
student/
main.go ← student implements this
reference/
walk.py ← Python reference verifier
test_lab.py ← autograder (three test functions)
fixtures/
01_valid.json ← valid attestation
02_bad_sig.json ← corrupted: signature mismatch
03_bad_chain.json ← corrupted: chain_hash mismatch
04_bad_witness.json ← corrupted: content_hash mismatch
05_bad_supports.json ← corrupted: unresolved supports
06_bad_targets.json ← corrupted: unresolved refutation targets
07_no_seal.json ← corrupted: seal block absent
keypair.pem ← Ed25519 key pair for fixtures
capstone/
rubric.md
interface_contracts/
Every lab directory contains a README.md. Read it first. It defines the deliverable, the acceptance criteria, and any lab-specific constraints. The autograder enforces acceptance criteria; the README explains intent. --- ## D.3 — Per-Lab Summary | Lab | Chapter | Title | Language | Key Deliverable | Run Command | |---|---|---|---|---|---| | 5 | Ch 5 | SHA-256 From Scratch | Python | hash.py passing all vectors | pytest test_lab.py | | 6 | Ch 6 | Ed25519 Sign & Verify | Python | signer.py passing all vectors | pytest test_lab.py | | 7 | Ch 7 | RFC 8785 Canonicalization | Python | canonicalize.py passing all vectors + cyberphone subset | pytest test_lab.py | | 8 | Ch 8 | Pydantic Schema Validation | Python | validator.py passing all fixtures | pytest test_lab.py | | 9 | Ch 9 | Embeddings & pgvector | Python | embed.py with cosine similarity queries | pytest -m db test_lab.py | | 10 | Ch 10 | Hybrid Retrieval (RRF) | Python | retriever.py passing recall benchmarks | pytest test_lab.py | | 11 | Ch 11 | LLM Extraction | Python | extractor.py on sample emails | pytest test_lab.py | | 12 | Ch 12 | Adversarial Prompting | Python | challenger.py generating five challenge types | pytest test_lab.py | | 13 | Ch 13 | Ingest Pipeline | Python | end-to-end ingest of sample corpus | pytest test_lab.py | | 14 | Ch 14 | RBAC & Privilege | SQL + Python | schema migrations + access tests | pytest -m db test_lab.py | | 15 | Ch 15 | Audit Log | Python | hash-chained audit entries | pytest test_lab.py | | 16 | Ch 16 | Epistemic Neutrality Masking | Python | masker.py on loaded claims | pytest test_lab.py | | 17 | Ch 17 | Tri-Model Consensus | Python | consensus.py with mock models | pytest test_lab.py | | 18 | Ch 18 | Refutation Block | Python | complete Refutation with coverage | pytest test_lab.py | | 19 | Ch 19 | Challenge Types | Python | all five challenge types exercised | pytest test_lab.py | | 20 | Ch 20 | Attestation Kinds | Python | emit one of each kind | pytest test_lab.py | | 21 | Ch 21 | emit.py End-to-End | Python | emit.py round-trip with walk | pytest test_lab.py | | 22 | Ch 22 | Chain of Attestations | Python | linked attestation sequence | pytest test_lab.py | | 23 | Ch 23 | Production & Privilege | Python + SQL | production.py with redactions | pytest -m db test_lab.py | | 24 | Ch 24 | FRE Admissibility Audit | Python | auditor.py on five fixtures | pytest test_lab.py | | 25 | Ch 25 | Canon Seven-Step Verifier | Go + Python | student/main.go passing all seven fixtures | pytest test_lab.py | | 26 | Ch 26 | SearchAttestation | Python | searcher.py emitting RRF results | pytest test_lab.py | | 27 | Capstone | End-to-End System | Python + SQL + Go | full pipeline from raw bytes to verified brief | see capstone/rubric.md | Labs 5–24 are placeholders pending chapter drafts. Lab 7 and Lab 25 are the two fully implemented labs in the current codebase. --- ## D.4 — Autograder Mechanics pytest is the autograder for all labs. There is no separate submission system. A lab passes when pytest exits with code 0. A lab fails when it
exits with a non-zero code.
Running the full suite for a lab:
cd docs/textbook/labs/ch25_verifier
pytest test_lab.py
Running one fixture:
pytest test_lab.py -k 01_valid
Running one test function:
pytest test_lab.py::test_python_matches_expectation
Lab 25 also requires building Go. The go_binary pytest fixture does this automatically before any test that needs the student binary, but only if go
is on your PATH. If the build fails, pytest fails with the Go compiler's
error output. To build manually:
cd docs/textbook/labs/ch25_verifier
go build -o verifier_student ./student
The three-assertion pattern in Lab 25. The test_lab.py for Lab 25 contains exactly three parametrized test functions, one per fixture: 1. test_python_matches_expectation — runs the Python reference verifier (reference/walk.py) and checks that it produces the documented verdict and, for invalid fixtures, the documented failing step. 2. test_go_matches_expectation — runs the student's compiled Go binary and applies the same checks. 3. test_python_and_go_agree — runs both and checks that the verdict is identical and that the failing step is identical. The EXPECTATIONS dict at the top of test_lab.py is the ground truth. It maps each fixture filename to (expected_verdict, expected_failing_step): | Fixture | Expected Verdict | Expected Failing Step | |---|---|---| | 01_valid.json | valid | — | | 02_bad_sig.json | invalid | step2_signature_verify | | 03_bad_chain.json | invalid | step3_chain_hash_recompute | | 04_bad_witness.json | invalid | step4_witness_content_hashes | | 05_bad_supports.json | invalid | step5_supports_resolution | | 06_bad_targets.json | invalid | step6_refutation_targets | | 07_no_seal.json | invalid | step0_seal_present | The failing-step determination. The _failing_step helper in test_lab.py scans the steps dict in the verifier's JSON output and returns the name of the first step whose value either starts with "fail" or has a failed count greater than 0. A verifier whose output uses different step names than the ones in EXPECTATIONS will fail test_python_and_go_agree even if its verdicts are correct. Database-touching labs require Postgres to be running. They are marked with pytest.mark.db. Skip them without Postgres:
pytest -m "not db" test_lab.py
D.5 — Submission Checklist by Tier
Every lab has three tiers. Each tier is cumulative: completing Stretch implies Core, which implies Warm-up.
Warm-up
- The code runs without raising an unhandled exception.
- The autograder does not crash (it may still report assertion failures).
- Any required output files exist (e.g.,
student/forge.jsonfor Lab 7 P7.6). Core -pytest test_lab.pyexits with code 0. - All parametrized test cases pass, including the full cyberphone subset (Lab 7) or all seven fixtures (Lab 25). - No tests are markedxfailor skipped except those skipped by a missing system dependency (e.g.,go not installed). Stretch -student/findings.mdexists and is at least one substantive paragraph (200+ characters) documenting what you discovered. - For Lab 7 Problem 7.6: the forge demonstrates a real parser-mismatch disagreement where Alice and Bob accept the same bytes but read different values. A null result (no disagreement found) is acceptable iffindings.mdhonestly documents the search. - For Lab 25 Stretch:findings.mddocuments either a fixture where your Go verifier caught a bug the Python reference missed, or a detailed explanation of a case where they agreed but for subtly different reasons. --- ## D.6 — Common Failure Modes and Fixes | Error | Cause | Fix | |---|---|---| |PEM decode failedorValueError: Could not deserialize key data| Key file is not RFC 8410 PEM, or is the wrong key type | Runmeridian-canon keygento regenerate; confirm the PEM header isBEGIN PUBLIC KEY(RFC 8410 SubjectPublicKeyInfo), notBEGIN ED25519 PUBLIC KEY| |chain_hash mismatchat step 3 | Canonicalization bug — keys sorted wrong, number formatted wrong, or whitespace emitted | Run the cyberphone vectors in Lab 7 first to validate your canonicalize implementation independently | |go build failed: module not found|go.modmodulepath does not match the directory path, or a required import is misspelled | Check thatgo.modinstudent/declaresmodule verifier_student(or whatever matches your import paths) | |fixture not found| Running pytest from the wrong directory | Runpytest test_lab.pyfromlabs/ch25_verifier/, not from the repo root or fromdocs/textbook/| |student binary not found| Go build step did not run or produced output in a different directory | Rungo build -o verifier_student ./studentfromlabs/ch25_verifier/explicitly, then rerun pytest | |connection refused(Postgres labs) | Postgres is not running | macOS:brew services start postgresql@16. Linux:systemctl start postgresql| |extension "vector" does not exist| pgvector not installed or not enabled in this database |CREATE EXTENSION IF NOT EXISTS vector;in the target database; install package if missing | |ModuleNotFoundError: No module named 'meridian'| Package not installed | Runpip install -e ".[test]"from the repo root | |CanonicalizationError: lone surrogate| Input contains a lone surrogate code point | This is the expected behavior for Lab 7 P7.4; your code should raise this error, not catch it | |assert py["verdict"] == go["verdict"]fails | Go verifier returns a different verdict than Python on the same fixture | Debug with./verifier_student fixtures/NN_xxx.jsondirectly; compare JSON output topython reference/walk.py fixtures/NN_xxx.json| |assert _failing_step(py) == _failing_step(go)fails | Both verifiers agree on verdict but name the failing step differently | Check your Go output'sstepskeys against the expected step names inEXPECTATIONS; they must match exactly | |forge.json missing required keys| Student's forge file is incomplete |forge.jsonmust containbase,duplicate_key,duplicate_first, andduplicate_last| |Alice and Bob agree on your forge| Parser-mismatch exploit did not produce divergent views | Re-readparser_mismatch_demo.pyanddemo_alt_parser.py; the duplicate-key position matters | --- ## D.7 — The Conformance Suite (Planned) Thenora-canon-conformance-suitepackage is on the Phase C–H roadmap. When published, it will provide a standalone test harness that any Canon verifier implementation can run, regardless of language:
pip install nora-canon-conformance-suite
canon-verify --implementation path/to/your/verifier
The conformance suite will include the fixtures from Lab 25, additional edge cases from the spec's normative examples, and a machine-readable report of which requirements (R1–R9) each tested implementation satisfies.
Until the conformance suite is published, Lab 25's test_lab.py is the canonical interop test. --- ## D.8 — Lab 25 Interop Test Specifics Lab 25 is the flagship cross-language lab. It has more moving parts than any other lab. This section covers the mechanics in detail. The three test functions correspond to the three verification assertions from the lab's purpose statement: test_python_matches_expectation is the sanity check. It verifies that the Python reference implementation (reference/walk.py) produces the documented verdict for every fixture. If this test fails, the bug is in the Python reference, not in the student's code. This is rare but not impossible: check meridian/canon/walk.py for recent changes. test_go_matches_expectation is the primary graded test. It builds the student's Go binary and runs it against every fixture. A student whose Go verifier disagrees with the expected verdict has a bug in their implementation. test_python_and_go_agree is the interop test. It checks that both verifiers
reach the same verdict and the same failing step for every fixture. A student
whose Go verifier correctly handles every fixture but reports a different step
name than the Python reference will fail here.
Reading the output. The verifiers must emit JSON to stdout. The expected schema is:
{
"verdict": "valid",
"steps": {
"step0_seal_present": "pass",
"step0_canon_version": "pass",
"step1_public_key_fetch": "pass",
"step2_signature_verify": "pass",
"step3_chain_hash_recompute": "pass",
"step4_witness_content_hashes": {"checked": 1, "failed": 0},
"step5_supports_resolution": {"checked": 1, "failed": 0},
"step6_refutation_targets": {"checked": 1, "failed": 0}
}
}
For an invalid fixture, verdict is "invalid" and exactly one step value starts with "fail" or has "failed": N where N > 0. Diagnosing disagreements. If Go passes but Python fails on the same fixture, you have a bug in the Python reference (reference/walk.py). This should be filed as a bug in the course repository, not fixed in student code. If Python passes but Go fails, the bug is in student/main.go. Run each
verifier directly on the failing fixture to inspect raw output:
python reference/walk.py fixtures/02_bad_sig.json
./verifier_student fixtures/02_bad_sig.json
Compare the steps dicts. The first step with a differing value is where the implementations diverge. The go_binary pytest fixture uses scope="module", meaning Go is compiled once per test session, not once per test case. If you edit student/main.go mid-session, rerun pytest from scratch rather than rerunning a single test case. Otherwise pytest will test your old binary. > § For the Record — Ed25519 verification in Go. > > The Go standard library provides Ed25519 in crypto/ed25519. The verify
call is:
ok := ed25519.Verify(publicKey, message, signature)where
messageis the UTF-8 bytes of the chain_hash string (including > the"sha256:"prefix),signatureis the base64-decoded signature > bytes, andpublicKeyis the 32-byte raw public key extracted from the PEM. Do not sign or verify the raw 32-byte hash; sign the string. See Appendix B, section B.7 for the exact byte sequence.