Skip to content

Instantly share code, notes, and snippets.

@Maadhav
Created July 13, 2026 13:17
Show Gist options
  • Select an option

  • Save Maadhav/e9d7bc9a5ce772e3e21c9eca942c1cd8 to your computer and use it in GitHub Desktop.

Select an option

Save Maadhav/e9d7bc9a5ce772e3e21c9eca942c1cd8 to your computer and use it in GitHub Desktop.
G2P — Interactive Flow: SEDESOL blockchain traceability demo
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>G2P — How It Works (Interactive Flow)</title>
<style>
:root {
--bg:#f4f6f6; --surface:#fff; --surface-2:#eef2f1; --ink:#15211f; --muted:#566461; --faint:#8a9794;
--border:#dce3e1; --border-strong:#c4cecb; --accent:#0c8375; --accent-strong:#096055; --accent-soft:#e0f0ec;
--good:#17795e; --good-soft:#e2f1ea; --warn:#9a6b12; --warn-soft:#f6ecd6; --danger:#b23636; --danger-soft:#f5e0de;
--mono-bg:#10201d; --mono-ink:#cfe6e0;
--shadow:0 1px 2px rgba(20,40,36,.05),0 6px 20px -8px rgba(20,40,36,.12);
--radius:12px; --maxw:960px;
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--mono:ui-monospace,"SF Mono","JetBrains Mono","Menlo","Consolas",monospace;
}
@media (prefers-color-scheme:dark){:root{
--bg:#0d1413; --surface:#141d1b; --surface-2:#1b2624; --ink:#e9efed; --muted:#9aa9a6; --faint:#6b7a77;
--border:#26312e; --border-strong:#33413d; --accent:#35c3b0; --accent-strong:#4dd3c1; --accent-soft:#10322c;
--good:#46c294; --good-soft:#102b22; --warn:#d8a743; --warn-soft:#2e2712; --danger:#e88; --danger-soft:#331c1c;
--mono-bg:#0a1413; --mono-ink:#bfe0d8; --shadow:0 1px 2px rgba(0,0,0,.3),0 8px 26px -10px rgba(0,0,0,.5);
}}
:root[data-theme="light"]{--bg:#f4f6f6;--surface:#fff;--surface-2:#eef2f1;--ink:#15211f;--muted:#566461;--faint:#8a9794;--border:#dce3e1;--border-strong:#c4cecb;--accent:#0c8375;--accent-strong:#096055;--accent-soft:#e0f0ec;--good:#17795e;--good-soft:#e2f1ea;--warn:#9a6b12;--warn-soft:#f6ecd6;--danger:#b23636;--danger-soft:#f5e0de;--mono-bg:#10201d;--mono-ink:#cfe6e0;--shadow:0 1px 2px rgba(20,40,36,.05),0 6px 20px -8px rgba(20,40,36,.12);}
:root[data-theme="dark"]{--bg:#0d1413;--surface:#141d1b;--surface-2:#1b2624;--ink:#e9efed;--muted:#9aa9a6;--faint:#6b7a77;--border:#26312e;--border-strong:#33413d;--accent:#35c3b0;--accent-strong:#4dd3c1;--accent-soft:#10322c;--good:#46c294;--good-soft:#102b22;--warn:#d8a743;--warn-soft:#2e2712;--danger:#e88;--danger-soft:#331c1c;--mono-bg:#0a1413;--mono-ink:#bfe0d8;--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 26px -10px rgba(0,0,0,.5);}
*{box-sizing:border-box;}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.6;-webkit-font-smoothing:antialiased;font-size:16px;}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 22px 96px;}
.eyebrow{font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);font-weight:600;}
header{padding:58px 0 28px;border-bottom:1px solid var(--border);}
header .eyebrow{display:block;margin-bottom:16px;}
h1{font-size:clamp(28px,5vw,44px);line-height:1.05;letter-spacing:-.02em;margin:0 0 14px;font-weight:800;text-wrap:balance;}
h1 .accent{color:var(--accent);}
.lede{font-size:clamp(15.5px,2.1vw,18px);color:var(--muted);max-width:64ch;margin:0;}
.meta-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px;}
.chip{font-family:var(--mono);font-size:11.5px;padding:5px 10px;border-radius:999px;border:1px solid var(--border-strong);color:var(--muted);background:var(--surface);white-space:nowrap;}
.chip b{color:var(--ink);font-weight:600;}
section{padding-top:50px;}
.sec-head{display:flex;align-items:baseline;gap:14px;margin-bottom:8px;}
.sec-num{font-family:var(--mono);font-size:13px;color:var(--accent);font-weight:600;padding-top:4px;}
h2{font-size:clamp(21px,3.3vw,28px);letter-spacing:-.015em;margin:0;font-weight:750;text-wrap:balance;}
.sub{color:var(--muted);margin:0 0 20px;max-width:66ch;}
p{margin:0 0 14px;} strong{color:var(--ink);font-weight:650;}
code{font-family:var(--mono);font-size:.84em;background:var(--surface-2);padding:1px 5px;border-radius:5px;border:1px solid var(--border);}
/* actor cards */
.actors{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;}
.actor{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:15px 16px;box-shadow:var(--shadow);border-top:3px solid var(--accent);}
.actor .ai{font-size:20px;line-height:1;}
.actor h3{margin:8px 0 2px;font-size:14.5px;letter-spacing:-.01em;}
.actor .ar{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint);}
.actor p{font-size:12px;color:var(--muted);margin:8px 0 0;}
.actor.oversight{border-top-color:var(--warn);} .actor.payer{border-top-color:var(--good);} .actor.citizen{border-top-color:var(--accent-strong);} .actor.audit{border-top-color:var(--faint);}
/* SIMULATOR */
.sim{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;margin-top:6px;}
.rail{display:grid;grid-template-columns:repeat(6,1fr);gap:0;background:var(--surface-2);border-bottom:1px solid var(--border);}
.stage{border:none;background:none;font-family:var(--sans);cursor:pointer;padding:12px 6px;text-align:center;position:relative;color:var(--faint);border-right:1px solid var(--border);}
.stage:last-child{border-right:none;}
.stage:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;}
.stage .sn{font-family:var(--mono);font-size:10px;display:block;}
.stage .st{font-size:11.5px;font-weight:600;letter-spacing:-.01em;display:block;margin-top:2px;line-height:1.2;}
.stage.done{color:var(--accent-strong);} .stage.done .sn{color:var(--accent);}
.stage.active{color:var(--ink);background:var(--surface);} .stage.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--accent);}
.stage.active .st{color:var(--accent-strong);}
.simbody{display:grid;grid-template-columns:1.1fr .9fr;gap:0;}
.happens{padding:20px 22px;border-right:1px solid var(--border);}
.h-actor{display:flex;align-items:center;gap:10px;margin-bottom:4px;}
.h-actor .badge{font-size:22px;}
.h-actor .hi{font-weight:700;font-size:15px;} .h-actor .hr{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);}
.h-action{font-size:14px;color:var(--ink);margin:10px 0 16px;}
.h-cols{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
.h-col{border-radius:9px;padding:12px 13px;font-size:12px;}
.h-col.off{background:var(--surface-2);border:1px solid var(--border);}
.h-col.on{background:var(--accent-soft);border:1px solid var(--accent);}
.h-col .cl{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;display:block;}
.h-col.off .cl{color:var(--faint);} .h-col.on .cl{color:var(--accent-strong);}
.h-col .cv{color:var(--muted);} .h-col.on .cv{color:var(--ink);}
.h-note{margin-top:14px;font-size:12.5px;color:var(--muted);border-left:2px solid var(--accent);padding-left:12px;}
.h-note strong{color:var(--ink);}
.explorer{padding:16px 18px;background:var(--mono-bg);color:var(--mono-ink);}
.explorer .ex-h{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#7fb8ac;display:flex;justify-content:space-between;margin-bottom:10px;}
.ex-list{display:flex;flex-direction:column;gap:7px;min-height:180px;}
.ex-empty{color:#5c7a74;font-family:var(--mono);font-size:11.5px;padding:24px 4px;text-align:center;}
.ex-row{border:1px solid #24413b;border-radius:8px;padding:8px 10px;background:rgba(255,255,255,.02);animation:pop .28s ease;}
@keyframes pop{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion:reduce){.ex-row{animation:none;}}
.ex-row .r1{display:flex;justify-content:space-between;align-items:center;gap:8px;}
.ex-row .est{font-family:var(--mono);font-size:11px;font-weight:700;color:#8fe0cf;}
.ex-row .esign{font-family:var(--mono);font-size:9.5px;color:#7fb8ac;}
.ex-row .r2{font-family:var(--mono);font-size:10px;color:#6fa89d;margin-top:5px;display:flex;flex-wrap:wrap;gap:2px 10px;}
.ex-row .hh{color:#b6d8d0;} .ex-row .prev{color:#5c7a74;}
.ex-row .pii{color:#e0c37a;}
.ex-anchor{border-color:#5a4a1a;background:rgba(216,167,67,.06);} .ex-anchor .est{color:#e6c46a;}
.simctl{display:flex;align-items:center;gap:10px;padding:14px 18px;border-top:1px solid var(--border);background:var(--surface-2);flex-wrap:wrap;}
.btn{font-family:var(--sans);font-size:13px;font-weight:600;padding:8px 16px;border-radius:8px;border:1px solid var(--border-strong);background:var(--surface);color:var(--ink);cursor:pointer;}
.btn:hover{border-color:var(--accent);} .btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff;}
.btn.primary:hover{background:var(--accent-strong);}
.btn:disabled{opacity:.4;cursor:not-allowed;}
.simctl .pos{font-family:var(--mono);font-size:12px;color:var(--muted);margin-left:auto;}
/* batch context bar */
.batchbar{display:flex;flex-wrap:wrap;gap:0;background:var(--surface-2);border-bottom:1px solid var(--border);}
.batchbar .bb{padding:9px 16px;border-right:1px solid var(--border);flex:1 1 auto;}
.batchbar .bl{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);display:block;margin-bottom:1px;}
.batchbar .bv{font-size:12.5px;font-weight:650;color:var(--ink);font-family:var(--mono);white-space:nowrap;}
/* glossary */
.gloss{display:grid;grid-template-columns:1fr 1fr;gap:2px 22px;}
.gt{display:flex;flex-direction:column;padding:12px 0;border-bottom:1px solid var(--border);}
.gterm{font-family:var(--mono);font-size:13px;font-weight:700;color:var(--accent-strong);margin-bottom:3px;}
.gdef{font-size:13px;color:var(--muted);}
.gdef strong{color:var(--ink);}
@media (max-width:760px){.gloss{grid-template-columns:1fr;}}
/* diagram / on-off */
pre{margin:16px 0 0;background:var(--mono-bg);color:var(--mono-ink);font-family:var(--mono);font-size:11.5px;line-height:1.5;padding:16px 18px;overflow-x:auto;border-radius:10px;}
.split2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px;}
.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow);}
.panel h3{margin:0 0 10px;font-size:13px;font-family:var(--mono);text-transform:uppercase;letter-spacing:.06em;}
.panel.on h3{color:var(--accent-strong);} .panel.off h3{color:var(--muted);}
.panel ul{margin:0;padding-left:16px;} .panel li{font-size:13px;color:var(--muted);margin-bottom:5px;} .panel li code{font-size:11.5px;}
.rules{display:flex;flex-direction:column;gap:8px;margin-top:16px;}
.rulerow{display:flex;gap:11px;font-size:13.5px;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:11px 14px;box-shadow:var(--shadow);}
.rulerow .rm{color:var(--accent);font-family:var(--mono);font-weight:700;flex-shrink:0;} .rulerow strong{color:var(--ink);}
/* segment matrix */
.table-scroll{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);margin-top:6px;}
table{border-collapse:collapse;width:100%;min-width:640px;background:var(--surface);font-size:13px;}
th,td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:top;}
thead th{font-family:var(--mono);font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;color:var(--faint);background:var(--surface-2);font-weight:600;}
tbody tr:last-child td{border-bottom:none;} td strong{color:var(--ink);}
.seg-l{font-family:var(--mono);font-weight:700;color:var(--accent);}
/* TAMPER widget */
.tamper{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;margin-top:6px;}
.tamper .tgrid{display:grid;grid-template-columns:1fr 1fr;gap:0;}
.tside{padding:18px 20px;}
.tside.left{border-right:1px solid var(--border);}
.tside h3{margin:0 0 12px;font-size:12px;font-family:var(--mono);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);}
.frow{display:flex;justify-content:space-between;gap:10px;font-size:12.5px;padding:5px 0;border-bottom:1px dashed var(--border);}
.frow:last-of-type{border-bottom:none;} .frow .fk{color:var(--faint);font-family:var(--mono);font-size:11px;} .frow .fv{color:var(--ink);}
.frow .fv .off{font-family:var(--mono);font-size:10px;color:var(--faint);}
.amt-edit{margin-top:12px;} .amt-edit label{font-size:11px;font-family:var(--mono);text-transform:uppercase;letter-spacing:.05em;color:var(--faint);display:block;margin-bottom:5px;}
.amt-edit input{font-family:var(--mono);font-size:15px;padding:8px 10px;border-radius:8px;border:1px solid var(--border-strong);background:var(--bg);color:var(--ink);width:150px;}
.amt-edit input:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent);}
.amt-edit .hint{font-size:11px;color:var(--faint);margin-top:6px;}
.hashrow{margin-bottom:12px;}
.hashrow .hk{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint);display:block;margin-bottom:3px;}
.hashrow .hv{font-family:var(--mono);font-size:12px;word-break:break-all;padding:8px 10px;border-radius:7px;background:var(--surface-2);border:1px solid var(--border);color:var(--ink);}
.hashrow.locked .hv{background:var(--mono-bg);color:var(--mono-ink);}
.verdict{margin-top:4px;padding:14px 16px;border-radius:9px;font-weight:650;font-size:14px;display:flex;align-items:center;gap:10px;}
.verdict .vi{font-size:20px;}
.verdict.ok{background:var(--good-soft);color:var(--good);border:1px solid var(--good);}
.verdict.bad{background:var(--danger-soft);color:var(--danger);border:1px solid var(--danger);}
.verdict small{display:block;font-weight:400;font-size:11.5px;opacity:.85;margin-top:2px;}
.anchornote{margin-top:16px;background:var(--warn-soft);border:1px solid var(--warn);border-radius:var(--radius);padding:14px 18px;font-size:13px;color:var(--muted);}
.anchornote strong{color:var(--ink);} .anchornote .al{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--warn);font-weight:700;display:block;margin-bottom:5px;}
footer{margin-top:52px;padding-top:24px;border-top:1px solid var(--border);color:var(--muted);font-size:12.5px;}
footer .fmark{color:var(--accent);font-family:var(--mono);}
@media (max-width:760px){
.simbody,.tamper .tgrid,.split2{grid-template-columns:1fr;}
.happens{border-right:none;border-bottom:1px solid var(--border);}
.tside.left{border-right:none;border-bottom:1px solid var(--border);}
.h-cols{grid-template-columns:1fr;}
.rail{grid-template-columns:repeat(6,minmax(84px,1fr));overflow-x:auto;}
header{padding-top:38px;}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="eyebrow">National G2P Platform · SEDESOL Honduras · Interactive Flow</span>
<h1>How the money moves — and why <span class="accent">no one can fake the record.</span></h1>
<p class="lede">A visual walkthrough of one scholarship (Becas) payment batch: how it flows from upload to disbursement, exactly what each institution signs, what lands on the blockchain versus a normal database, and a live tamper test you can break yourself. Step through it below.</p>
<div class="meta-row">
<span class="chip"><b>Batch</b> · BATCH-2026-00192</span>
<span class="chip"><b>Program</b> · Becas</span>
<span class="chip"><b>Payers</b> · Banco A · Coop B</span>
<span class="chip"><b>Non-custodial</b> · records, not money</span>
</div>
</header>
<section>
<div class="sec-head"><span class="sec-num">01</span><h2>Who's in the room</h2></div>
<p class="sub">Five kinds of participant. The core idea: each one signs its own part of every payment with its own cryptographic key — so trust doesn't rest on any single institution.</p>
<div class="actors">
<div class="actor"><div class="ai">🏛️</div><h3>SEDESOL</h3><div class="ar">Executing institution</div><p>Runs the program. Its staff hold 3 roles: <strong>Operator</strong> (uploads), <strong>Verifier</strong> (audits), <strong>Authorizer</strong> (approves).</p></div>
<div class="actor oversight"><div class="ai">🔎</div><h3>SEFIN / SIAFI</h3><div class="ar">Oversight &amp; funds</div><p>Confirms budget is accrued (<strong>Devengado</strong>) and <strong>signs that fact onto the chain</strong> — it acts as the "oracle" — before anything transmits. Watches, reconciles; doesn't operate.</p></div>
<div class="actor payer"><div class="ai">🏦</div><h3>Payer Agents</h3><div class="ar">Banco A · Coop B</div><p>Actually move the money to beneficiary accounts, then <strong>sign back</strong> the paid/rejected result.</p></div>
<div class="actor citizen"><div class="ai">👤</div><h3>Beneficiary</h3><div class="ar">The citizen</div><p>Receives the funds and <strong>confirms receipt</strong> (SMS/USSD) — the independent last-mile check.</p></div>
<div class="actor audit"><div class="ai">⚖️</div><h3>Auditor / TSC</h3><div class="ar">Read-only</div><p>Verifies the whole trail independently — against the blockchain, without trusting SEDESOL's copy.</p></div>
</div>
</section>
<section>
<div class="sec-head"><span class="sec-num">02</span><h2>Step through one batch</h2></div>
<p class="sub">Click <strong>Next</strong> (or a stage) to advance the batch through its lifecycle. Watch what's written to the normal database vs. what's committed to the blockchain — the mini explorer on the right fills up as attestations are signed.</p>
<div class="sim">
<div class="batchbar">
<div class="bb"><span class="bl">Batch</span><span class="bv">BATCH-2026-00192</span></div>
<div class="bb"><span class="bl">Program</span><span class="bv">Becas · Q3 2026</span></div>
<div class="bb"><span class="bl">Beneficiaries</span><span class="bv">40</span></div>
<div class="bb"><span class="bl">Total</span><span class="bv">HNL 100,000.00</span></div>
<div class="bb"><span class="bl">Budget doc</span><span class="bv">F01-99231 / 01</span></div>
<div class="bb"><span class="bl">Payers</span><span class="bv">Banco A · Coop B</span></div>
</div>
<div class="rail" id="rail"></div>
<div class="simbody">
<div class="happens">
<div class="h-actor"><span class="badge" id="hBadge">📄</span><div><div class="hi" id="hActor">—</div><div class="hr" id="hInst">—</div></div></div>
<div class="h-action" id="hAction">—</div>
<div class="h-cols">
<div class="h-col off"><span class="cl">Off-chain · Postgres</span><span class="cv" id="hOff">—</span></div>
<div class="h-col on"><span class="cl">On-chain · Besu</span><span class="cv" id="hOn">—</span></div>
</div>
<div class="h-note" id="hNote">—</div>
</div>
<div class="explorer">
<div class="ex-h"><span>⛓ On-chain ledger</span><span id="exCount">0 attestations</span></div>
<div class="ex-list" id="exList"><div class="ex-empty">Nothing anchored yet — advance the batch to begin the signed trail.</div></div>
</div>
</div>
<div class="simctl">
<button class="btn" id="btnPrev">◀ Back</button>
<button class="btn primary" id="btnNext">Next ▶</button>
<button class="btn" id="btnReset">Reset</button>
<span class="pos" id="pos">Step 1 / 6</span>
</div>
</div>
</section>
<section>
<div class="sec-head"><span class="sec-num">03</span><h2>How the blockchain actually works here</h2></div>
<p class="sub">The chain is a parallel, PII-free notary that <em>mirrors</em> the database. Every state change writes to Postgres <strong>first</strong> and drops an event in an outbox in the same transaction; an async relayer commits a thin attestation to the chain. If the chain is down, money still flows — it just catches up later.</p>
<pre>
Operator / Verifier / Authorizer (browser · no keys)
│ action
┌──────────────────────────────────────────────┐
│ App / Batch Engine │
│ setState() ─ ONE DB txn ─► Postgres (truth) │ ◄─ AUTHORITATIVE · on critical path
│ └► outbox row │
└──────┬──────────────────────────┬─────────────┘
│ (request path) │ (async · OFF critical path)
▼ ▼
Payer Agents ◄─signed─► Relayer / HSM signer
(Segment B out, │ commits attestation
Segment C back) ▼
Besu / QBFT · thin smart contract
{ batch, status, docHash,
initiator, approver, HMAC(DNI) }
Nightly Merkle root ─► Bitcoin (OpenTimestamps)
</pre>
<div class="split2">
<div class="panel on"><h3>On the chain</h3><ul>
<li>batch id (opaque), status, <code>docHash</code></li>
<li>signer addresses (initiator, approver), role, timestamp</li>
<li>salted <code>HMAC(DNI)</code> — never the real DNI</li>
<li>segment signature hashes</li>
</ul></div>
<div class="panel off"><h3>Off the chain (encrypted DB)</h3><ul>
<li>DNI, name, mobile, account number/type</li>
<li><strong>per-beneficiary amounts</strong>, geography</li>
<li>the documents themselves</li>
<li>the HMAC salts (destroy to erase → habeas data)</li>
</ul></div>
</div>
<div class="rules">
<div class="rulerow"><span class="rm">▹</span><span>The smart contract <strong>reverts an Authorize if the signer is the same person who created the batch</strong> — segregation of duties enforced by the network, not just the app.</span></div>
<div class="rulerow"><span class="rm">▹</span><span>It also <strong>refuses to advance without the prior Verified + FundValidated attestations</strong> — the Devengado guardrail, backstopped on-chain.</span></div>
<div class="rulerow"><span class="rm">▹</span><span><strong>No money and no business rules live on-chain</strong> — only proofs. Eligibility and amounts stay in patchable code; the chain just witnesses.</span></div>
</div>
</section>
<section>
<div class="sec-head"><span class="sec-num">04</span><h2>Each institution validates its own segment</h2></div>
<p class="sub">Reconciliation = these four independently-signed segments agreeing on one batch. No central system has to expose an API — each party <em>writes and signs</em> its own piece.</p>
<div class="table-scroll">
<table>
<thead><tr><th>Segment</th><th>Signed by</th><th>What it attests</th><th>How it's verified</th></tr></thead>
<tbody>
<tr><td><span class="seg-l">A</span></td><td><strong>SEFIN / SIAFI</strong></td><td>Funds are accrued (Devengado) for this F01 document</td><td>SIAFI oracle key signature on the <code>FundValidated</code> attestation</td></tr>
<tr><td><span class="seg-l">B</span></td><td><strong>SEDESOL executing unit</strong></td><td>The exact instruction content sent to the bank (<code>docHash</code>)</td><td>Re-hash the payload → must equal the on-chain <code>docHash</code></td></tr>
<tr><td><span class="seg-l">C</span></td><td><strong>Payer Agent (bank/coop)</strong></td><td>Each payment's real outcome — Paid / Rejected / Reversed</td><td>Bank's signed callback (JWS); the bank cannot later repudiate it</td></tr>
<tr><td><span class="seg-l">D</span></td><td><strong>Beneficiary</strong></td><td>The money actually arrived (cash-in-hand)</td><td>SMS/USSD confirmation — an independent check on the bank's claim</td></tr>
</tbody>
</table>
</div>
<p style="margin-top:14px;font-size:13px;color:var(--muted);">Approvals within SEDESOL (Operator → Verifier → Authorizer) form a fifth, internal chain-of-custody — each transition signed by a <em>different</em> role key, which is what makes the audit timeline unforgeable. <strong>Segment A</strong> is where SEFIN acts as the platform's <em>“oracle”</em>: it doesn't need a live SIAFI API — a signed confirmation (automated or by an authorized officer) is enough to put the funds check on-chain.</p>
</section>
<section>
<div class="sec-head"><span class="sec-num">05</span><h2>Try to break it — the tamper test</h2></div>
<p class="sub">When the batch was transmitted, the platform committed a real SHA-256 hash of Juan Pérez's payment record to the chain. That hash is now frozen. Change the amount below and hit Verify — the recomputed hash won't match, and the tampering is caught instantly. (The hashing is real SHA-256, computed live in your browser.)</p>
<div class="tamper">
<div class="tgrid">
<div class="tside left">
<h3>Off-chain record (editable)</h3>
<div class="frow"><span class="fk">beneficiary</span><span class="fv">Juan Pérez</span></div>
<div class="frow"><span class="fk">dni</span><span class="fv">0801-1990-12345 <span class="off">(off-chain)</span></span></div>
<div class="frow"><span class="fk">account</span><span class="fv">••••••7890 <span class="off">(off-chain)</span></span></div>
<div class="amt-edit">
<label for="amt">amount (HNL) — try changing it</label>
<input id="amt" type="text" inputmode="decimal" value="2500.00" autocomplete="off" spellcheck="false">
<div class="hint">e.g. change 2500.00 → 25000.00 and watch the check fail.</div>
</div>
</div>
<div class="tside right">
<h3>Verification</h3>
<div class="hashrow locked"><span class="hk">On-chain commitment (immutable)</span><span class="hv" id="lockedHash">computing…</span></div>
<div class="hashrow"><span class="hk">Recomputed from current record</span><span class="hv" id="liveHash">computing…</span></div>
<div class="verdict ok" id="verdict"><span class="vi">✓</span><div>Verified — record matches the chain<small>The off-chain data is provably the same bytes that were anchored.</small></div></div>
<button class="btn primary" id="btnVerify" style="margin-top:14px;">Re-verify</button>
</div>
</div>
</div>
<div class="anchornote">
<span class="al">And the chain itself?</span>
Someone could try to rewrite the blockchain instead of the database — but every night the ledger's Merkle root is stamped onto <strong>Bitcoin</strong> via OpenTimestamps. To forge the history, an attacker would have to rewrite Bitcoin too. That's why <strong>even SEDESOL and the auditor cannot silently alter the record</strong> — the trail is provable to anyone, in-country or at the IMF, without trusting the government's copy.
</div>
</section>
<section>
<div class="sec-head"><span class="sec-num">06</span><h2>Plain-words glossary</h2></div>
<p class="sub">The terms above, one line each — handy when you're narrating the demo to a non-technical audience.</p>
<div class="gloss">
<div class="gt"><span class="gterm">Oracle</span><span class="gdef">The trusted bridge that carries an off-chain fact (e.g. "funds are accrued in SIAFI") onto the chain by <strong>signing it</strong>. Here, <strong>SEFIN is the oracle</strong>.</span></div>
<div class="gt"><span class="gterm">Attestation</span><span class="gdef">A signed statement written on-chain: "this happened, I vouch for it, at this time." Each step adds one.</span></div>
<div class="gt"><span class="gterm">docHash</span><span class="gdef">A SHA-256 fingerprint of a document/payload. Same bytes → same hash; change one character → a totally different hash.</span></div>
<div class="gt"><span class="gterm">HMAC(DNI)</span><span class="gdef">A keyed, salted fingerprint of a national ID — lets the chain reference a person <strong>without ever storing the real DNI</strong>.</span></div>
<div class="gt"><span class="gterm">Merkle root / anchor</span><span class="gdef">One hash that summarizes a whole day of records. Stamping it onto Bitcoin makes that day's ledger impossible to back-date.</span></div>
<div class="gt"><span class="gterm">Devengado</span><span class="gdef">Spanish for "accrued" — the SIAFI budget stage where funds are formally committed to a payment. The platform's hard guardrail.</span></div>
<div class="gt"><span class="gterm">Segregation of duties</span><span class="gdef">Whoever creates a batch cannot be the one who approves it — enforced here by the <strong>smart contract</strong>, not just policy.</span></div>
<div class="gt"><span class="gterm">Non-custodial</span><span class="gdef">The platform never holds or moves money — banks do. It records and proves; it doesn't touch a single Lempira.</span></div>
</div>
</section>
<footer>
<span class="fmark">▹</span> Companion to the Architecture Guide and the SEDESOL SRS Review. Illustrative walkthrough — signer addresses and batch IDs are sample data; the SHA-256 hashing and tamper detection in §05 are real and run entirely in your browser.
</footer>
</div>
<script>
(function(){
"use strict";
var enc = new TextEncoder();
async function sha256hex(str){
try{
if(window.crypto && window.crypto.subtle){
var buf = await window.crypto.subtle.digest('SHA-256', enc.encode(str));
var arr = Array.from(new Uint8Array(buf));
return arr.map(function(b){return b.toString(16).padStart(2,'0');}).join('');
}
}catch(e){}
// fallback (labeled non-crypto) — FNV-1a, only if SubtleCrypto is unavailable
var h = 0x811c9dc5;
for(var i=0;i<str.length;i++){ h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
var s = (h>>>0).toString(16).padStart(8,'0');
var out=''; while(out.length<64){ out += s; s = (Math.imul(parseInt(s,16)>>>0,16777619)>>>0).toString(16).padStart(8,'0'); }
return out.slice(0,64);
}
function shortHash(h){ return h.slice(0,10) + '…' + h.slice(-6); }
var RAIL = ['Draft','Verified','Fund Validation','Authorized','Transmitted','Reconciled'];
var STEPS = [
{ badge:'📄', actor:'Operator · Ana Discua', inst:'SEDESOL · Unidad de Becas',
action:'Uploads the Becas Q3 batch — 40 scholarships, HNL 100,000.00 total, against budget doc F01-99231/01. e.g. Juan Pérez (Tegucigalpa) 2,500 · María López (San Pedro Sula) 2,500 · José Rodríguez (La Ceiba) 2,500 …',
off:'40 payment-line rows + batch header saved to Postgres. DNI, names, and account numbers encrypted at rest.',
on:'— nothing anchored yet.',
note:'Draft is still editable, so nothing hits the chain yet — anchoring data that is still changing would be pointless.' },
{ badge:'🔍', actor:'Verifier · Luis Fúnez', inst:'SEDESOL · Unidad de Becas',
action:'Reviews all 40 rows against the roster (DNI formats, duplicates, amounts) — finds 0 discrepancies — and signs off at 10:02.',
off:'Batch → Verified; verifier id + timestamp recorded.',
on:'Attestation #1 — <b>Verified</b>, signed by the Verifier key.',
note:'First anchor. The Verifier’s signing key is cryptographically <strong>distinct from the Operator’s</strong> — segregation of duties starts on the chain here.' },
{ badge:'💰', actor:'SEFIN · Funds Attestation', inst:'SEFIN — Presupuesto (the “oracle”)',
action:'SEFIN confirms F01-99231 is accrued (Devengado): available HNL 3,480,000 ≥ this batch’s HNL 100,000. HNL 100,000 is earmarked so it can’t back a second batch.',
off:'Devengado result + accrued balance cached; earmark row placed against (F01-99231, seq 01).',
on:'Attestation #2 — <b>FundValidated</b>, signed by SEFIN’s funds-attestation key (Segment A).',
note:'<strong>What’s an “oracle”?</strong> The blockchain can’t read SIAFI by itself, so SEFIN checks the funds and <strong>signs the result onto the chain</strong> — giving proof the funds existed and who vouched for them. No live SIAFI API needed: if there isn’t one, a SEFIN officer confirms it by hand and that signed confirmation is the attestation.' },
{ badge:'✅', actor:'Authorizer · Carla Mejía', inst:'SEDESOL · Unidad de Becas',
action:'Sees the Devengado light turn GREEN and gives final approval at 11:20 — which triggers transmission.',
off:'Batch → Authorized.',
on:'Attestation #3 — <b>Authorized</b>, signed by the Authorizer key.',
note:'The smart contract <strong>reverts if the signer is the same person who created the batch</strong>, and requires the prior Verified + FundValidated attestations. Enforced by the network, not just the app.' },
{ badge:'📤', actor:'Executing Unit', inst:'SEDESOL → Banco A · Coop B',
action:'The signed instruction goes out: 30 items → Banco A (REST API), 10 items → Coop B (ISO-20022 over SFTP).',
off:'Outbound payload delivered on each agent’s channel; the platform now awaits callbacks.',
on:'Attestation #4 — <b>Sent</b> + <code>docHash</code> = SHA-256 of the exact payload (Segment B).',
note:'The <code>docHash</code> freezes precisely what was sent, byte-for-byte — that’s the record you can try to break in §05.' },
{ badge:'🔄', actor:'Payer Agents + Beneficiaries', inst:'Banco A · Coop B · Citizens',
action:'Callbacks return: Banco A pays 30/30; Coop B pays 8/10 (2 rejected — “cuenta inactiva”). 36 beneficiaries confirm receipt by SMS.',
off:'Per-item outcomes ingested; control totals reconcile: 38 paid (HNL 95,000) + 2 rejected (HNL 5,000) = HNL 100,000 instructed.',
on:'Attestations #5–7 — <b>Paid ×38 / Rejected ×2</b> signed by the banks (Segment C), beneficiary receipts (Segment D), then the day’s Merkle root anchored to Bitcoin.',
note:'Four independent parties have each signed their piece. <strong>No single one — not even SEDESOL — can rewrite the trail</strong>, and the 2 rejected items route to a retry queue.' }
];
// on-chain ledger entries, keyed by the step index at which they appear
var LOG = [
{ step:1, status:'Verified', signer:'Verifier', detail:'40 items attested', block:482103, ts:'10:02', amt:'HNL 100,000.00' },
{ step:2, status:'FundValidated', signer:'SEFIN Funds Attest', detail:'F01-99231 accrued ✓', block:482106, ts:'10:20', amt:'HNL 100,000.00' },
{ step:3, status:'Authorized', signer:'Authorizer', detail:'approver ≠ initiator ✓', block:482109, ts:'11:20', amt:'HNL 100,000.00' },
{ step:4, status:'Sent', signer:'Executing Unit', detail:'docHash of payload', block:482111, ts:'11:35', amt:'HNL 100,000.00' },
{ step:5, status:'Paid ×38', signer:'Banco A', detail:'Segment C · cleared', block:482140, ts:'14:30', amt:'HNL 95,000.00' },
{ step:5, status:'Rejected ×2', signer:'Coop B', detail:'Segment C · cuenta inactiva', block:482141, ts:'14:31', amt:'HNL 5,000.00' },
{ step:5, status:'Anchored → BTC', signer:'Merkle→BTC', detail:'OpenTimestamps · BTC #851,204', block:482160, ts:'23:00', amt:'—', anchor:true }
];
var addrs = {}, chained = [], committedHash = '', ready = false, cur = 0;
var BATCH = 'BATCH-2026-00192';
var record = { name:'Juan Pérez', dni:'0801199012345', account:'1234567890', amount:'2500.00' };
function canonical(r){ return r.name + '|' + r.dni + '|' + r.account + '|' + r.amount; }
var $ = function(id){ return document.getElementById(id); };
async function init(){
var roles = ['Operator','Verifier','SEFIN Funds Attest','Authorizer','Executing Unit','Banco A','Coop B','Beneficiary','Merkle→BTC'];
for(var i=0;i<roles.length;i++){
var a = await sha256hex(roles[i] + ':sedesol-g2p');
addrs[roles[i]] = '0x' + a.slice(0,8) + '…' + a.slice(8,12);
}
var prev = '0'.repeat(64);
for(var j=0;j<LOG.length;j++){
var e = LOG[j];
var body = JSON.stringify({ s:e.status, by:addrs[e.signer]||e.signer, batch:BATCH, seq:j+1, prev:prev });
var h = await sha256hex(body);
chained.push({ prev:prev, hash:h });
prev = h;
}
committedHash = await sha256hex(canonical(record));
ready = true;
buildRail();
render();
initTamper();
}
function buildRail(){
var rail = $('rail'); rail.innerHTML = '';
RAIL.forEach(function(name, i){
var b = document.createElement('button');
b.className = 'stage'; b.type = 'button';
b.innerHTML = '<span class="sn">'+(i+1)+'</span><span class="st">'+name+'</span>';
b.addEventListener('click', function(){ cur = i; render(); });
rail.appendChild(b);
});
}
function render(){
var s = STEPS[cur];
// rail
var stages = document.querySelectorAll('.stage');
stages.forEach(function(el, i){
el.classList.toggle('active', i === cur);
el.classList.toggle('done', i < cur);
});
// happens panel
$('hBadge').textContent = s.badge;
$('hActor').textContent = s.actor;
$('hInst').textContent = s.inst;
$('hAction').textContent = s.action;
$('hOff').textContent = s.off;
$('hOn').innerHTML = s.on;
$('hNote').innerHTML = s.note;
// explorer
var list = $('exList');
var shown = [];
for(var i=0;i<LOG.length;i++){ if(LOG[i].step <= cur) shown.push(i); }
if(shown.length === 0){
list.innerHTML = '<div class="ex-empty">Nothing anchored yet — advance the batch to begin the signed trail.</div>';
} else {
list.innerHTML = '';
shown.forEach(function(idx){
var e = LOG[idx], c = chained[idx];
var row = document.createElement('div');
row.className = 'ex-row' + (e.anchor ? ' ex-anchor' : '');
row.innerHTML =
'<div class="r1"><span class="est">'+ (e.anchor ? '⚓ ' : '#'+(idx+1)+' ') + e.status +'</span>'+
'<span class="esign">'+ e.signer +' · '+ (addrs[e.signer]||'') +'</span></div>'+
'<div class="r2"><span class="prev">blk #'+ e.block +'</span><span class="prev">'+ e.ts +'</span><span class="hh">'+ e.amt +'</span></div>'+
'<div class="r2"><span class="hh">tx '+ shortHash(c.hash) +'</span>'+
'<span class="prev">prev '+ shortHash(c.prev) +'</span>'+
'<span class="'+(e.anchor?'':'pii')+'">'+ (e.anchor ? e.detail : 'PII: HMAC only · '+e.detail) +'</span></div>';
list.appendChild(row);
});
}
$('exCount').textContent = shown.length + (shown.length === 1 ? ' attestation' : ' attestations');
// controls
$('btnPrev').disabled = (cur === 0);
$('btnNext').disabled = (cur === STEPS.length - 1);
$('pos').textContent = 'Step ' + (cur+1) + ' / ' + STEPS.length;
}
async function verifyTamper(){
if(!ready) return;
record.amount = $('amt').value.trim();
var live = await sha256hex(canonical(record));
$('liveHash').textContent = live;
$('lockedHash').textContent = committedHash;
var v = $('verdict');
if(live === committedHash){
v.className = 'verdict ok';
v.innerHTML = '<span class="vi">✓</span><div>Verified — record matches the chain<small>The off-chain data is provably the same bytes that were anchored.</small></div>';
} else {
v.className = 'verdict bad';
v.innerHTML = '<span class="vi">✕</span><div>TAMPER DETECTED — hash mismatch<small>The record was altered after it was anchored. The blockchain commitment does not lie.</small></div>';
}
}
function initTamper(){
$('lockedHash').textContent = committedHash;
$('amt').addEventListener('input', verifyTamper);
$('btnVerify').addEventListener('click', verifyTamper);
verifyTamper();
}
$('btnNext').addEventListener('click', function(){ if(cur < STEPS.length-1){ cur++; render(); } });
$('btnPrev').addEventListener('click', function(){ if(cur > 0){ cur--; render(); } });
$('btnReset').addEventListener('click', function(){ cur = 0; render(); });
init();
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment