"""๐Ÿ•ต๏ธ Hack the trace tab: solvi makes a loan decision and publishes its hash-chained trace; the player edits it to flip the answer, and the verifier shows which defence catches the forgery (logic in games/tracehack.py). app.py calls `build()` inside `with gr.Tab("๐Ÿ•ต๏ธ Hack the trace"):` and appends `CSS` to the page's CSS. The attempts / successes counter lives in the visitor's browser (gr.BrowserState โ†’ localStorage).""" from __future__ import annotations import html import json import gradio as gr from games import tracehack as th from games.explain import why_html DEFAULT_APPLICANT = next(iter(th.APPLICANTS)) DEFAULT_LEVEL = next(iter(th.LEVELS)) CSS = """ .th-receipt {font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.5; word-break: break-all;} .th-verdict {font-size: 1.05rem; font-weight: 700; margin-bottom: 6px;} .th-checks {list-style: none; padding: 0; margin: 6px 0;} .th-checks li {margin: 3px 0; font-size: 0.88rem;} .th-checks li .th-msg {display: block; font-family: var(--font-mono); font-size: 0.78rem; opacity: 0.9; margin-left: 22px;} .th-table {width: 100%; border-collapse: collapse; font-size: 0.8rem; margin-top: 6px;} .th-table th, .th-table td {border-top: 1px solid var(--border-color-primary); padding: 3px 6px; text-align: left; vertical-align: top;} .th-table td.th-mono {font-family: var(--font-mono);} .th-table tr.th-bad td {background: #dc262618;} .th-table td.th-edited {background: #f59e0b30; font-weight: 700;} .th-counter {font-size: 0.95rem;} """ WHY = """ **Why this matters.** When an AI system decides something about a person (a loan, a claim, a diagnosis), someone will later ask *"why, and is that really what the system computed?"* A log can be edited. A solvi trace is harder to fake: every step records the hashes of its inputs and of its value, and points to the hash of the step before it, starting from the hash of the input. `trace.replay(catalog)` re-executes every step from its recorded inputs, so it catches a changed value even when all hashes were recomputed. What replay cannot know is *which* input was the real one; for that the system publishes a short receipt (the input hash and the final hash) at decision time, to the applicant and to an append-only log. Together: the trace explains the decision, replay proves the explanation is the computation, the receipt proves it is the same computation. | level | what you do | what catches it | |---|---|---| | 1 | edit a value | the record's own hash; replay | | 2 | edit values and rehash those records | the hash chain (next record's `prev`); replay | | 3 | edit values and rebuild the whole chain | replay: the value โ‰  the value recomputed from its inputs | | 4 | change an input, re-run honestly, rebuild the chain | the published receipt (a different input hash) | | bonus | delete a step so replay cannot recompute its reader | the planned flow (and the receipt) | """ def _receipt_html(receipt, applicant): return (f"
๐Ÿ“œ Published receipt (at decision time, for {html.escape(applicant)})" f"
answer: {receipt['answer']}
input hash: {receipt['init_hash']}
" f"final hash: {receipt['head']}
steps: {receipt['steps']}
" "
The goal: make the trace below say " "approve and pass every check.
") def _level_md(level): task, defence = th.LEVELS[level] return f"**Your task.** {task}\n\n
Which defence catches it?\n\n{defence}\n
" def _counter_md(counts): counts = counts or {} return (f"
๐Ÿ•ต๏ธ In this browser: {counts.get('attempts', 0)} forgery attempts ยท " f"{counts.get('successes', 0)} successes
") def _table(d, original, bad_steps): orig = {r.get("step"): r for r in original["records"]} rows = [] for r in d["records"]: o = orig.get(r.get("step"), {}) bad = r.get("step") in bad_steps cells = [] for key, mono in (("step", False), ("kind", False), ("name", True), ("value", True), ("prev", True), ("hash", True)): v = r.get(key) txt = json.dumps(v, ensure_ascii=False) if key == "value" else str(v) if key in ("prev", "hash"): txt = txt[:10] + "โ€ฆ" edited = o.get(key) != v cls = " ".join(x for x in ("th-mono" if mono else "", "th-edited" if edited else "") if x) cells.append(f"{html.escape(txt)}") status = "โŒ " + html.escape("; ".join(m.split(": ", 1)[-1] for m in bad_steps[r.get("step")])) if bad else "โœ…" rows.append(f"{''.join(cells)}{status}") init_edit = d["init"] != original["init"] head = ("stepkindnamevalueprevhashreplay") note = ("
amber = changed from the published trace" + (" ยท the input (init) was changed too" if init_edit else "") + "
") return f"{head}{''.join(rows)}
{note}" def _verdict_html(v, original): if v.get("parse_error"): return f"
Cannot read the trace: {html.escape(v['parse_error'])}
" d = v["doc"] if not v["tampered"]: head = "โœ… The published trace is untouched: every check passes, the answer is reject. Now try to forge it." elif v["ok"] and v["changed"]: head = f"๐Ÿ† Every check passes and the answer is now {html.escape(str(v['answer']))}. You beat it (please report how)!" elif v["ok"]: head = "โœ… Every check passes, but the answer is still reject: nothing was forged." else: label, msg = v["first"] head = (f"๐Ÿšจ Forgery caught by {label}: {html.escape(msg)}" + ("" if v["changed"] else " (and the answer did not even change)")) items = [] for key, label, desc in th.CHECKS: msgs = v["checks"][key] mark = "โŒ" if msgs else "โœ…" src = "solvi replay" if key in ("record", "chain", "inputs", "recompute") else "arcade verifier" items.append(f"
  • {mark} {label} โ€” {desc} ({src})" + "".join(f"{html.escape(m)}" for m in msgs[:4]) + "
  • ") replay = ("trace.replay(catalog): no mismatches" if v["replay_ok"] else "trace.replay(catalog): mismatches") return (f"
    {head}
    {replay} " f"the trace claims: {html.escape(str(v['answer']))}" f"{_table(d, original, v['bad_steps'])}
    ") # -------------------------------------------------------------------------------------------------------------------- # events # -------------------------------------------------------------------------------------------------------------------- def new_decision(applicant): resp, d, receipt = th.decide(applicant) title = f"solvi's decision for {th.APPLICANTS[applicant]['applicant']}: {resp['decision'].answer}" card = why_html(resp, th.cat, ["decision"], title=title) first = _verdict_html(th.verify(th.dumps(d), d, receipt), d) return card, _receipt_html(receipt, th.APPLICANTS[applicant]["applicant"]), th.dumps(d), first, d, receipt def do_verify(text, original, receipt, counts): counts = dict(counts or {"attempts": 0, "successes": 0}) v = th.verify(text, original, receipt) if not v.get("parse_error") and v["tampered"]: counts["attempts"] = counts.get("attempts", 0) + 1 if v["ok"] and v["changed"]: counts["successes"] = counts.get("successes", 0) + 1 return _verdict_html(v, original), counts, _counter_md(counts) def _helper(fn): def run(text, *args): try: return fn(text, *args), gr.update() except th.TraceFormatError as e: return gr.update(), f"
    The helper could not run: {html.escape(str(e))}
    " return run def build(): gr.Markdown( "solvi decides a loan and publishes its **hash-chained trace**: every step with the hashes of its inputs, its " "value, the hash of the step before (`prev`) and its own `hash`. **Your challenge:** edit the trace so it says " "`approve` and still passes `trace.replay(catalog)`. You may change anything, and the forger's helpers below " "rebuild hashes for you. Pick a level for a hint, or press *Show me the attack* to see it done and caught.") resp0, d0, receipt0 = th.decide(DEFAULT_APPLICANT) original = gr.State(d0) receipt = gr.State(receipt0) counts = gr.BrowserState({"attempts": 0, "successes": 0}, storage_key="solvi_arcade_tracehack_counter", secret="solvi-arcade-tracehack-v1") with gr.Row(): with gr.Column(scale=5, min_width=320): with gr.Row(equal_height=True): applicant = gr.Dropdown(list(th.APPLICANTS), value=DEFAULT_APPLICANT, label="applicant", scale=3) new_btn = gr.Button("New decision", scale=1, min_width=110) decision = gr.HTML(why_html(resp0, th.cat, ["decision"], title=f"solvi's decision for {th.APPLICANTS[DEFAULT_APPLICANT]['applicant']}: " f"{resp0['decision'].answer}")) receipt_box = gr.HTML(_receipt_html(receipt0, th.APPLICANTS[DEFAULT_APPLICANT]["applicant"])) level = gr.Radio(list(th.LEVELS), value=DEFAULT_LEVEL, label="level") level_md = gr.Markdown(_level_md(DEFAULT_LEVEL)) attack_btn = gr.Button("๐ŸŽฌ Show me the attack (applies it to the trace)") counter = gr.HTML(_counter_md(None)) with gr.Column(scale=7): editor = gr.Code(value=th.dumps(d0), language="json", lines=18, max_lines=30, interactive=True, label="the published trace (edit anything)") with gr.Row(equal_height=True): step_no = gr.Number(value=5, precision=0, minimum=1, maximum=20, label="step N", scale=1, min_width=80) rehash_btn = gr.Button("#๏ธโƒฃ Rehash step N", scale=2) chain_btn = gr.Button("๐Ÿ”— Recompute the whole hash chain", scale=3) with gr.Row(): rerun_btn = gr.Button("๐Ÿ” Re-run the steps from the inputs") reset_btn = gr.Button("โ†ฉ Reset to the published trace") verify_btn = gr.Button("๐Ÿ” Verify: replay the trace", variant="primary") verdict = gr.HTML(_verdict_html(th.verify(th.dumps(d0), d0, receipt0), d0)) with gr.Accordion("Why this matters, and which defence catches what", open=False): gr.Markdown(WHY) new_btn.click(new_decision, applicant, [decision, receipt_box, editor, verdict, original, receipt]) applicant.change(new_decision, applicant, [decision, receipt_box, editor, verdict, original, receipt]) level.change(_level_md, level, level_md) attack_btn.click(lambda lv, o: th.attack(lv, o), [level, original], editor).then( do_verify, [editor, original, receipt, counts], [verdict, counts, counter]) rehash_btn.click(_helper(lambda t, n: th.rehash_step(t, int(n or 0))), [editor, step_no], [editor, verdict]) chain_btn.click(_helper(th.rechain), editor, [editor, verdict]) rerun_btn.click(_helper(th.rerun), editor, [editor, verdict]) reset_btn.click(lambda o: th.dumps(o), original, editor) verify_btn.click(do_verify, [editor, original, receipt, counts], [verdict, counts, counter]) gr.on(triggers=None, fn=_counter_md, inputs=[counts], outputs=counter) # on page load (reads localStorage) and on change