Created
July 13, 2026 13:17
-
-
Save Maadhav/e9d7bc9a5ce772e3e21c9eca942c1cd8 to your computer and use it in GitHub Desktop.
G2P — Interactive Flow: SEDESOL blockchain traceability demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 & 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