Plan spajanja s fazom 1
Redoslijed, dan prijelaza, izvor rezultata, /g/<id>, povratak, E2E (26.9.2026.)
# 8. Integracija s fazom 1: plan spajanja
Status (26. 9. 2026.): dan D je napravljen. Faza 1 je zatvorena u 01:20:41 UTC (03:20 po
zagrebačkom vremenu), a glasanje se nastavlja na Gnosis Chainu (MaksimirGlasanjeV1
0x812960FA1120121DEd82A8806aECE93dcf49E869). Sve iz ovog plana je na produkciji i u main
(PR #2, domovina-api PR #4). Tijek i hashevi: Put do produkcije, korak 7.
Povijest implementacije: Stanje implementacije. Sažetak za javnost:
Dan D: prijelaz na Gnosis.
Ovaj dokument spaja MaksimirGlasanjeV1 s glasanjem koje radi na produkciji
od 25. 9. 2026. (faza 1). Dijelovi su već opisani u
04 i u 01 „Prijelaz s faze 1”. Ovdje su
složeni u jedan redoslijed, a dodani su zastavica, izvor rezultata, objave /g/<id>, checkpoint
Action, maksimir_verify.py --chain, povratak i E2E.
Ukratko:
- Sve se najprije spaja i testira na Chiadu, s pravom eOsobnom na produkcijskoj domeni i iza zastavice. Tek zatim ide deploy na Gnosis, jer se ugovor nakon deploya ne može popraviti (samo V2).
- Faza 1 se ne briše i ne prepisuje. Na dan prijelaza (D) prestaje primati listiće, zadnji snapshot ide u Bitcoin, a lanac faze 1 postaje javan kao i svaki zatvoren lanac.
- Jedna osoba ima jedan glas preko obje faze. Registrar pri upisu na lanac u lanac faze 1 zapisuje povlačenje starog listića („prijenos”), pa se isti glas ne može brojati dvaput, a to svatko može provjeriti iz javnog lanca.
- Postojeći ZK ključ iz faze 1 postaje ključ za lanac: isti commitment, bez nove tajne.
- Rezultat je lanac + ostatak faze 1 (listići koji nisu preneseni). Oba dijela su provjerljiva bez nas: lanac iz događaja ugovora, a ostatak iz javnog lanca faze 1 i OTS snapshotova.
- Na produkciji danas nema nijednog aktivnog listića (Matijin je povučen pri testu novog UI-ja). Prijelaz je zato jeftin ako se napravi prije šire promocije glasanja.
# Polazno stanje (26. 9. 2026.)
| Dio | Stanje | Izvor |
|---|---|---|
| Faza 1, lanac listića | vrh seq 2, 0 aktivnih glasača |
javni RPC maksimir_snapshot() |
| Faza 1, ZK grupa | 1 član (Matijin ključ), seq 1 |
maksimir_zk_head() |
| Faza 1, zadnji checkpoint u repou | seq 1 (25. 9. 17:43 UTC); seq 2 čeka sljedeći run |
glasanje/checkpoints/index.json |
| Checkpoint cron | radi, ali neredovito (zakazani runovi 17:56 i 21:33 UTC, ne svaki sat) | gh run list -w maksimir-checkpoint |
| V1 na Chiadu | 0xe790…e029, verificiran, E2E kroz kod relayera |
deployments/chiado/v1.json |
| V1 na Gnosisu | nije deployan | — |
| Relayer | kôd i testovi, nije deployan (26. 9. premješten u Worker maksimir) |
web/worker/relayer/ |
Registrar, maksimir_keystore |
ne postoje | 04 |
| Web | Vite + Worker maksimir; Astro je odbačen |
odluka |
# Ciljna slika
flowchart LR
subgraph B["Preglednik glasača"]
K["ključ: 24 riječi + passkey<br/>(ili ključ iz faze 1)"]
D["ZK dokaz listića"]
K --> D
end
subgraph W["Cloudflare"]
WEB["Worker maksimir<br/>/glasanje, /g/id"]
REL["Worker maksimir-relayer<br/>plaća gas"]
end
subgraph API["domovina-api"]
CERT["edge fn certilia<br/>(eOsobna)"]
REG["edge fn maksimir-register<br/>EIP-712 potpis"]
DB[("Postgres<br/>faza 1 (zamrznuta)<br/>+ registracije, keystore,<br/>objave /g/id")]
end
subgraph G["Gnosis Chain"]
V1["MaksimirGlasanjeV1"]
end
GA["GitHub Action<br/>snapshot → OTS → Bitcoin"]
B -- "prijava" --> CERT
B -- "commitment" --> REG
REG --> DB
REG -- "potpis" --> B
B -- "paket" --> REL --> V1
B -. "čita grupu, listić, zbroj" .-> V1
WEB -. "OG kartica" .-> DB
WEB -. "OG kartica" .-> V1
GA -. "faza 1 + stanje lanca" .-> DB
GA -.-> V1
Poslužitelj i dalje zna samo ono što je znao u fazi 1: tko je potvrđen i koji je commitment upisao. Listić i zbroj više nisu u bazi, nego na lancu.
# Načela
- Jedan glas po osobi preko obje faze. Nijedan korak ne smije omogućiti da se ista osoba broji i u fazi 1 i na lancu.
- Ništa iz faze 1 se ne briše. Lanac hasheva, ZK zapisnik, snapshotovi i objave ostaju provjerljivi zauvijek.
- Chiado prije Gnosisa, za svaki dio. Gnosis se deploya tek kad je cijeli tok prošao na Chiadu s pravom eOsobnom.
- Povratak je moguć u svakom koraku. Do dana D povratak je samo gašenje zastavice. Nakon dana D listići na lancu se ne mogu obrisati, pa je povratak ograničen (vidi Povratak).
- Test ne dira korisnikove tajne. Matijin ZK ključ iz faze 1 je i njegov pravi ključ.
# Redoslijed
Zadani redoslijed bio je Gnosis → registrar → relayer → web. Predlažem da se registrar, relayer i web prvo spoje s Chiadom, a Gnosis deploy dođe tek nakon E2E s pravom eOsobnom. Razlog je nepromjenjivost ugovora: ako integracija otkrije da ugovoru nešto nedostaje (npr. događaj ili getter koji web treba), na Chiadu je to novi deploy, a na Gnosisu V2. Gnosis deploy je zatim mehanički korak po 03, a registrar, relayer i web samo mijenjaju konfiguraciju.
flowchart TD
O["0. Odluke<br/>(otvorena pitanja)"] --> R["1. Registrar + tablice<br/>domovina-api, Chiado ključ"]
R --> L["2. Relayer Worker<br/>Chiado"]
L --> W["3. Web tok iza zastavice<br/>?lanac=chiado"]
W --> V["4. verify --chain<br/>+ snapshot v3"]
V --> E1{"5. E2E Chiado<br/>prava eOsobna"}
E1 -- "pada" --> R
E1 -- "prolazi" --> G["6. Gnosis deploy V1<br/>Safe, tag, verifikacija"]
G --> C["7. registrar + relayer<br/>→ Gnosis konfiguracija"]
C --> E2{"8. E2E Gnosis<br/>(Matijin pravi glas)"}
E2 -- "prolazi" --> D["9. Dan D: prijelaz<br/>faza 1 zamrznuta"]
E2 -- "pada" --> C
D --> P["10. Nakon prijelaza<br/>praćenje, checkpoint, docs"]
| # | Korak | Repo | Test | Povratak |
|---|---|---|---|---|
| 1 | registrar + maksimir_chain_registrations + maksimir_keystore |
domovina-api |
SQL testovi (kao supabase/tests/20260925_*), Deno test edge funkcije |
migracija je aditivna; funkcija se ne poziva dok web ne zna za nju |
| 2 | relayer na Chiadu | ovaj (web/worker/relayer) |
postojeći testovi + /status na Chiadu |
wrangler delete |
| 3 | web tok | ovaj (web/) |
unit (happy-dom), glasanje-ui.mjs s presretnutim API-jem, npm run check |
zastavica isključena = faza 1 nepromijenjena |
| 4 | maksimir_verify.py --chain, snapshot v3 |
ovaj (scripts/) |
fiksture s Chiada, test u suprotnom smjeru | stara provjera radi kao i dosad |
| 5 | E2E Chiado s eOsobnom | — | kontrolna lista | — |
| 6 | Gnosis deploy | ovaj (chain/) |
check-frozen, verifikacija, manifest |
ništa se ne mijenja dok se konfiguracija ne prebaci |
| 7 | konfiguracija za Gnosis | oba | /status, registracija testnog ključa |
vratiti konfiguraciju na Chiado |
| 8 | E2E Gnosis | — | Matijin pravi glas na lancu | vidi Povratak |
| 9 | dan D | domovina-api (SQL), web |
runbook | Povratak |
# 1. Registrar i tablice (domovina-api)
Nova migracija 2026092xxxxxxx_maksimir_chain.sql, primijenjena ručno (kao i dosad, ne
db-migrate.sh). Sve je u shemi domovina_ai.
-- Pravo glasa na lancu: jedna osoba = jedan commitment po ugovoru.
create table domovina_ai.maksimir_chain_registrations (
chain_id bigint not null,
contract text not null check (contract ~ '^0x[0-9a-f]{40}$'),
oib_hash text not null,
commitment text not null check (commitment ~ '^[0-9]{1,78}$'),
created_at timestamptz not null default now(),
primary key (chain_id, contract, oib_hash),
unique (chain_id, contract, commitment)
);
-- Šifrirani omot ključa (ADR 0001). Nema korisnika, OIB-a ni commitmenta.
create table domovina_ai.maksimir_keystore (
credential_id_hash text primary key check (credential_id_hash ~ '^[0-9a-f]{64}$'),
blob jsonb not null,
created_at timestamptz not null default now()
);
- RLS zatvara obje tablice za
anoniauthenticated. Pristup ide samo kroz funkcije. - Keystore RPC-evi:
maksimir_keystore_put(hash, blob)imaksimir_keystore_get(hash), oba zaanon.putsamo dodaje: prvi upis pobjeđuje, a ponovni upis istog bloba vraća uspjeh. Kad bi se omot mogao prepisati, svatko tko znacredentialIdmogao bi glasaču pokvariti omot (ne bi ga ukrao, ali bi ga zaključao). Nova zaštita istog ključa znači novi passkey, dakle novicredentialId. Ograničenje veličine bloba (npr. 2 kB) sprječava zlouporabu tablice. maksimir_settingsdobiva dva stupca (vidi zastavicu):chain_from timestamptzichain jsonb({chainId, contract, relayer}za web).
# Edge funkcija maksimir-register
POST /functions/v1/maksimir-register Authorization: Bearer <sesija nakon eOsobne>
{ "chainId": 100, "commitment": "…" }
→ 200 { "chainId", "contract", "commitment", "deadline", "signature" }
→ 409 { "error": "already_registered", "commitment": "<postojeći>" }
→ 4xx { "error": "not_verified" | "terms_not_accepted" | "unknown_chain" | "invalid_commitment" | "commitment_taken" }
sequenceDiagram
autonumber
participant B as Preglednik
participant F as maksimir-register
participant DB as Postgres
B->>F: { chainId, commitment } + sesija
F->>DB: identity_verifications → oib_hash
F->>DB: maksimir_voters.consented_at (uvjeti v2)
F->>DB: _maksimir_chain_register_for(user, chainId, contract, commitment)
Note over DB: jedna transakcija:<br/>1. red u maksimir_chain_registrations (ili isti postojeći)<br/>2. ako je to produkcijski ugovor i glasač ima listić faze 1:<br/> red u maksimir_log s revision 0 („prijenos”)
DB-->>F: ok / already_registered / commitment_taken
F->>F: signTypedData(Register(commitment, deadline = sada + 1 h))
F-->>B: potpis
- Ključ registrara po mreži:
MAKSIMIR_REGISTRAR_KEY_CHIADOiMAKSIMIR_REGISTRAR_KEY_GNOSISkao tajne edge funkcije. Adresa ugovora dolazi izmaksimir_settings.chain, pa funkcija ne potpisuje za proizvoljan ugovor. - Isti commitment, isti odgovor. Ponovni zahtjev s istim commitmentom daje novi potpis (novi
rok), jer prvi možda nije stigao do lanca. Drugačiji commitment iste osobe daje
already_registered. Zato izgubljen ključ ne donosi drugi glas. - Prijenos listića faze 1 događa se pri registraciji, ne pri predaji na lancu, jer registrar ne vidi listić na lancu (nullifier ne zna). Web zato registraciju i prvu predaju radi kao jedan korak, s listićem faze 1 kao nacrtom (vidi prijenos).
viemradi u Denou (npm:viem), aregisterTypedDatase kopira izchain/client/ballot.ts. Test u ovom repou provjerava da potpis iz edge funkcije prolaziregisterna lokalnom Hardhatu.
# 2. Relayer
Izvedeno drukčije od prvog prijedloga (odluka 10): relayer je ruta /relayer/<chainId>/… u
Workeru maksimir, pa se web i relayer deployaju zajedno. Detalji su u
02:
- više mreža u jednom Workeru:
RELAYER_CHAINS+ tajnaSPONSOR_PRIVATE_KEY_<chainId>; mreža bez tajne vraća 503; - isti izvor kao web, pa nema CORS-a;
- najmanja napojnica 0,01 gwei (I-05);
- sponzor na Gnosisu: novi EOA s 1 xDAI. Uz izmjerenih ~0,5 M gasa po listiću i gornju granicu
relayera od 5 gwei (
MAX_FEE_GWEI), 1 xDAI pokriva najmanje 400 listića i u najgorem slučaju. Uz stvarnu naknadu od ~10 wei trošak je zanemariv (02).
# 3. Web tok
Web ostaje Vite + Worker. Kriptografija dolazi iz chain/client/ bez
kopiranja: web/vite.config.ts dobiva server.fs.allow: [".."], a web/package.json dodaje
viem i @scure/bip39 (iste verzije kao chain/). Sve se učitava dinamički, kao današnji
zk.ts, pa naslovnica ne raste.
Novi moduli, bez diranja postojećih dok zastavica nije uključena:
| Modul | Sadržaj |
|---|---|
web/src/chainConfig.ts |
mreža, adresa, groupId, blok deploya, URL relayera iz maksimir_settings.chain + chain/deployments/<mreža>/v1.json |
web/src/chainVote.ts |
ključ (keystore, uvoz iz faze 1), registracija, dokaz, relayer, ballotOf, rezultati s lanca |
web/src/chainVoteView.ts |
koraci i stanja za laike, po uzoru na demo |
Postojeći glasanjeView.ts dobiva samo grananje „faza 1 / lanac” i poziv novog pogleda. Galerija i
listić od 100 bodova (commit dd15f1f) ostaju isti. Mijenja se samo gumb „Predaj” i ono što
slijedi.
stateDiagram-v2
[*] --> Prijava: nije prijavljen
Prijava --> Uvjeti: eOsobna ok
Uvjeti --> Ključ: uvjeti v2 prihvaćeni
Ključ --> Ključ: ključ iz faze 1 → 24 riječi<br/>ili novi ključ → 24 riječi<br/>potvrda 3 riječi, passkey
Ključ --> Pravo: ključ spreman
Pravo --> Listić: registrar + relayer /register<br/>(jednom)
Listić --> Dokaz: 100 bodova raspodijeljeno
Dokaz --> Poslano: relayer /cast
Dokaz --> Paket: „pošalji sam” (preuzmi JSON)
Poslano --> Potvrđeno: transakcija u bloku,<br/>ballotOf = ovaj listić
Potvrđeno --> Listić: izmjena (revizija + 1)
Potvrđeno --> [*]
Svaki korak ima stanje čeka / sada / gotovo / greška, aktivan gumb samo za trenutni korak i
jednu rečenicu običnim jezikom („Tvoj preglednik sada računa dokaz. Traje oko 2 sekunde.”). Greške
relayera prevode se kao i RPC greške u glasanje.ts (BadRevision → „Listić je u međuvremenu
promijenjen s drugog uređaja. Učitavam novo stanje.”).
# Zastavica i prebacivanje
Jedna zastavica u bazi, bez novog deploya weba:
maksimir_settings |
Značenje |
|---|---|
chain_from is null |
faza 1 kao danas; tok na lancu vide samo testeri s ?lanac=chiado |
chain_from > now() |
najava: web prikazuje datum prijelaza, faza 1 i dalje prima listiće |
chain_from <= now() |
faza 1 zatvorena za listiće i ZK upis; web koristi lanac iz chain |
maksimir_results()vraća ichain_from,chainiphase1_residual, pa web jednim pozivom zna u kojoj je fazi._maksimir_is_open()za predaju listića dobiva uvjetnow() < coalesce(chain_from, 'infinity').closes_atostaje kraj cijelog glasanja (31. 12. 2027.) i jednak jeclosesAtugovora.maksimir_log()postaje javan odchain_from(lanac faze 1 je zatvoren), a ne tek odcloses_at.?lanac=chiado(zapamćeno usessionStorage) postoji samo za testere. Uz njega stranica jasno piše „TESTNA MREŽA, glas se ne broji”. Chiado nikad nije produkcijski izvor.
# Dan D: prijelaz
sequenceDiagram
autonumber
actor M as Matija
participant DB as Postgres
participant GA as Checkpoint Action
participant R as Repo
participant W as Web
M->>DB: update maksimir_settings set chain_from = now(), chain = '{Gnosis…}'
Note over DB: maksimir_cast_ballot, maksimir_zk_register,<br/>maksimir_zk_share: voting_closed
M->>GA: workflow_dispatch (force)
GA->>DB: maksimir_snapshot() → phase1_final
GA->>R: checkpoints/<UTC>-seqN.json + .ots
M->>DB: maksimir_log(), maksimir_zk_group() (sada javni)
M->>R: glasanje/faza1/lanac.json, zk_grupa.json
M->>M: maksimir_verify.py lanac.json checkpoints/*.json --zk zk_grupa.json
W-->>W: sljedeće učitavanje: tok na lancu
Note over GA: .ots dobiva Bitcoin atestaciju za nekoliko sati,<br/>prijelaz ne čeka na to
# Zatvaranje maksimir_cast_ballot
Zatvara se preko chain_from, ne brisanjem funkcije. Staro sučelje (stari JS u predmemoriji
preglednika) tako dobiva jasnu grešku voting_closed, a web je prevodi u „Glasanje se preselilo na
lanac. Osvježi stranicu.” Isto vrijedi za maksimir_zk_register i maksimir_zk_share: nove
anonimne objave idu na lanac.
maksimir_set_public ostaje otvoren: glasač i dalje smije uključiti ili isključiti javni prikaz
(vidi objave).
# Završni OTS snapshot
Ručni workflow_dispatch s force odmah nakon chain_from. Snapshot nosi phase1_final: true
(dio schema v3). To je stanje faze 1 koje se više ne mijenja, osim zapisa „prijenos” (sljedeći
odjeljak), koji su i sami u lancu hasheva.
# Ponovna predaja postojećih listića
Listić iz faze 1 ne može se prenijeti automatski, jer ga nije potpisao glasačev ključ (01). Tok:
- Glasač se prijavi.
maksimir_my_ballot()vraća njegov listić faze 1 (items), a web vidi da je faza 1 zatvorena. - Web kaže: „Tvoj glas iz faze 1 i dalje se broji. Prenesi ga na lanac da ga možeš mijenjati.” Listić faze 1 učita se kao nacrt.
- Ključ (sljedeći odjeljak), pa registracija. Registrar u istoj transakciji u lanac faze 1 upiše
red
revision 0(„prijenos”). Od tog trenutka listić faze 1 više se ne broji. - Odmah zatim dokaz i predaja na lancu, s nacrtom iz koraka 2.
Rizik: ako korak 4 ne uspije (relayer nedostupan, glasač zatvori stranicu), glas neko vrijeme ne
postoji ni u fazi 1 ni na lancu. Ublažavanje: nacrt ostaje u localStorage, stranica pri sljedećem
posjetu nudi „Dovrši prijenos”, a „preuzmi paket” omogućuje slanje bez relayera. Alternativa je
otvoreno pitanje 3.
Za zbroj to znači: ostatak faze 1 = listići faze 1 čiji zadnji red u lancu nije revision 0.
To je već pravilo lanca (povlačenje je revision 0), pa maksimir_verify.py ostatak računa bez
izmjene. Zapisi „prijenos” razlikuju se od običnog povlačenja samo po tome što nastaju nakon
chain_from.
# Ponovna upotreba ZK ključa iz faze 1
Ključ iz faze 1 (maksimir-zk-identity u localStorage, datoteka maksimir-zk-kljuc.txt) je
32 bajta, isto kao tajna u ADR 0001. Provjereno je
da base64 → 24 riječi → natrag daje isti commitment (fromPhase1Export).
| Situacija | Što web radi |
|---|---|
lokalni ključ postoji i njegov commitment = zk_commitment iz maksimir_my_ballot() |
„Tvoj ZK ključ iz faze 1 postaje ključ za lanac.” → prikaz 24 riječi, potvrda 3 riječi, passkey. Isti commitment ide registraru |
zk_commitment postoji, lokalnog ključa nema |
ponudi uvoz datoteke ili novi ključ; upozori da novi ključ znači da stara anonimna objava ostaje vezana uz stari ključ |
| glasač nije imao ZK ključ (većina) | novi ključ → 24 riječi → passkey |
Posljedice istog commitmenta:
- Commitment je već javan u ZK zapisniku faze 1, pa njegov upis na lanac ne otkriva ništa novo.
- Nullifier anonimne objave (
SHARE_SCOPE) isti je u fazi 1 i na lancu. Objava na lancu tog istog ključa pokazat će da je to ista osoba kao u objavi faze 1, što je istina i ne otkriva identitet. - Nullifier listića (
BALLOT_SCOPE) je drugačiji, pa se anonimna objava ne može povezati s listićem. - Na lancu nema zamjene ključa (V1 nema uklanjanja člana), za razliku od faze 1. Izgubljen ključ = zamrznut listić (06).
# Izvor rezultata
flowchart LR
subgraph P1["Faza 1 (Postgres, zamrznuto)"]
L["lanac listića<br/>javan od dana D"]
RES["ostatak = listići<br/>koji nisu preneseni"]
L --> RES
end
subgraph CH["Gnosis"]
V["V1.results()<br/>= zbroj BallotCast"]
end
RES --> T["UKUPNO<br/>po radu: bodovi, podupiratelji"]
V --> T
T --> WEB["/glasanje"]
T --> SNAP["snapshot v3 → Bitcoin"]
| Razdoblje | Izvor | Provjera |
|---|---|---|
| do dana D | baza (maksimir_results), kao danas |
lanac hasheva + OTS |
| od dana D | lanac (results() svih verzija iz chain/deployments/) + ostatak faze 1 |
lanac: događaji ugovora; ostatak: javni lanac faze 1 + OTS |
| nakon 31. 12. 2027. | isto, konačno | maksimir_verify.py --chain |
- Web čita lanac izravno iz preglednika (javni RPC Gnosisa, s rezervnim popisom), a ostatak iz
maksimir_results(). Prikazuje ukupno, a u „Kako se broji” oba dijela odvojeno. - Ostatak se nakon dana D može samo smanjivati (prijenosi). Ne može rasti, jer faza 1 ne prima listiće.
- Broj glasača =
votersna lancu + broj listića u ostatku. Dvostrukog brojanja nema, jer prijenos ukida listić faze 1 prije nego što ista osoba dobije pravo glasa na lancu.
# Objave /g/<id>
| Objava | Gdje živi | Nakon dana D |
|---|---|---|
| javna s imenom, listić faze 1 | baza | ostaje. Kad glasač prenese listić, kartica prikazuje listić s lanca (redak ispod) |
| anonimna ZK, faza 1 | baza (dokaz + zk_seq) |
ostaje valjana; provjerava se kao i dosad nad grupom faze 1 |
| javna s imenom, listić na lancu | baza: ime i dokaz vlasništva nullifiera; lanac: listić | nova |
| anonimna, na lancu | lanac: V1.share, događaj AnonymousShare; baza: samo kratka poveznica na transakciju |
nova |
Ime nikad ne ide na lanac. Za javnu objavu listića s lanca baza mora znati njegov nullifier, a glasač ne smije moći tuđi listić prikazati pod svojim imenom. Zato uz nullifier šalje i ZK dokaz vlasništva:
scope = BALLOT_SCOPE → isti nullifier kao listić
message = keccak256("maksimir-javno" | share_id) → vrijedi samo za ovu objavu
Baza sprema nullifier i dokaz (maksimir_set_public dobiva opcijske parametre). Posjetitelj u
pregledniku provjerava SNARK, scope, poruku i da je korijen bio korijen grupe na lancu, a zatim
čita ballotOf(nullifier). Takav dokaz ne može poslužiti kao listić (cast traži drugu poruku)
ni kao objava (share traži drugi scope). Nullifier tada postaje javno povezan s imenom, ali
samo po izboru glasača, kao i danas s listićem faze 1.
Anonimna objava na lancu: preglednik izradi proveShare, relayer pošalje /share, a baza
dobije samo (id, chain_id, tx_hash) preko anonimnog klijenta, kao i danas. Stranica objave
provjeru radi s lanca (događaj AnonymousShare u toj transakciji). Pravilo faze 1 „ZK je
onemogućen dok je glas javan” ostaje.
OG kartica (web/worker/share.ts) za objavu s lanca čita listić preko Gnosis RPC-a
(ballotOf), s istom predmemorijom od 60 s. Ako RPC ne odgovori, ostaje opća kartica.
# Satni checkpoint Action nakon prijelaza
Action ostaje i dobiva snapshot v3. Stari snapshotovi (v1, v2) se ne diraju.
| Polje v3 | Sadržaj |
|---|---|
| sve iz v2 | vrh lanca faze 1, vrh ZK zapisnika, rezultati ostatka faze 1 |
phase1_final |
true od dana D |
chain |
{ chainId, contract, block, blockHash, voters, registered, results } pročitano u istom bloku |
- Zašto i lanac u Bitcoin: Gnosis je sam javan i nepromjenjiv, pa to nije nužno za integritet.
Ali jedan snapshot tako veže oba izvora u istom trenutku, pa
maksimir_verify.py --chainmože provjeriti ukupan zbroj u svakom satu, a repozitorij ostaje aktivan (GitHub gasi cron nakon 60 dana mirovanja). - Kad piše: kad se promijeni vrh faze 1 ili
voters/blok s novimBallotCast, a inače jednom u 24 sata. - Kraj: nakon
closesAt(31. 12. 2027.) posljednji snapshotfinal: true, zatim se Action zaustavlja. - Neredovit cron (zakazani runovi ne dolaze svaki sat) rješava se prije dana D: rezervni
okidač iz Cloudflare Cron Triggera koji poziva
workflow_dispatch. Za dan D ionako ide ručni run.
# maksimir_verify.py --chain
Proširenje postojeće skripte, i dalje samo standardna biblioteka Pythona.
python3 scripts/maksimir_verify.py glasanje/faza1/lanac.json glasanje/checkpoints/*.json \
--zk glasanje/faza1/zk_grupa.json \
--chain chain/deployments/gnosis/v1.json --rpc https://rpc.gnosischain.com
- Iz manifesta čita adresu, blok deploya i
chainId. Provjerieth_chainIdi da je keccak256 koda na adresi jednakruntimeCodeKeccak256iz manifesta (nema proxyja, kôd je onaj iz repoa). eth_getLogszaBallotCastiMigratedod bloka deploya, u odsječcima. Ponovno zbraja: zadnji listić po nullifieru, bez preseljenih.eth_call results()na istom bloku i usporedi s ponovnim zbrajanjem.- Za svaki snapshot v3: zbroj iz događaja do
chain.blockmora biti jednakchain.results, ablockHashjednak onome s lanca. - Ukupno = ostatak faze 1 (iz lanca faze 1) + lanac. Ispis po radu.
SNARK dokaze skripta ne provjerava: to je ugovor učinio pri svakom cast, a Python nema pairing.
Standardna biblioteka nema keccak256 (hashlib.sha3_256 je drugačiji padding), pa skripta nosi
vlastitu implementaciju u čistom Pythonu (~40 linija) s testom prema poznatim vektorima. Iz nje
računa selektore, topic0 i hash koda. Testovi: fiksture s Chiada (stvarne transakcije iz
E2E) i provjera u suprotnom smjeru (izmijenjen results ili izbačen događaj → exit 1).
# Povratak
| Razina | Kada | Što | Posljedica |
|---|---|---|---|
| 0 | prije dana D | chain_from = null |
ništa; faza 1 radi kao danas |
| 1 | relayer ne radi ili je sponzor prazan | ugasi rutu relayera ili dopuni xDAI; web nudi „preuzmi paket” | listići se mogu slati sami; nitko ne gubi glas |
| 2 | registrarov ključ procurio | Safe: setRegistrar(novi); nova tajna u edge funkciji |
lažne registracije do tog trenutka vidljive su kao rast registered |
| 3 | greška u V1 | V2 prema 07, dobrovoljna selidba | V1 ostaje i broji se |
| 4 | cijeli put na lancu mora stati | chain_from = null → faza 1 ponovno prima listiće, ali maksimir_cast_ballot odbija osobe s redom u maksimir_chain_registrations |
listići na lancu ostaju i broje se; bez tog pravila ista osoba bi glasala dvaput |
Razina 4 zato traži da provjera registracije u _maksimir_cast_ballot_for postoji od prvog dana,
iako se koristi samo pri povratku.
# Privatnost
| Faza 1 | Na lancu | |
|---|---|---|
| Listić tijekom glasanja | skriven; javni samo zbroj i vrh lanca | javan pod nullifierom čim je predan |
| Vrijeme predaje | skriveno do zatvaranja | javno (blok) |
| Operater zna čiji je listić | da (voter_id → oib_hash) |
ne: zna samo commitment, a nullifier iz njega ne može izračunati |
| Tko je autor anonimne objave | operater posredno | nitko |
| Ime | samo uz javnu objavu | nikad na lancu |
Posljedice za dan D:
- Uvjeti v2. Tekst privole mijenja se („listić je javan pod pseudonimom čim ga predaš”), a
consented_atza lanac traži novu privolu (terms_version = 2). - Lanac faze 1 postaje javan na dan D. To je obećanje faze 1 (javan po zatvaranju). Zapis
„prijenos” u vremenu blizu registracije i prve predaje na lancu može povezati pseudonim faze 1 s
nullifierom na lancu. Oba su pseudonimi. Ublažavanje:
maksimir_log()vraćats_mszaokružen na sat za zapise nakon dana D (hash i dalje koristi točants_ms, pa ga provjera treba; zato je ovo otvoreno pitanje 6). - Serije u relayeru (paketi se šalju jednom na sat) smanjuju povezivanje po vremenu. Otvoreno pitanje iz README.
# E2E s pravom eOsobnom
Na Chiadu, na produkcijskoj domeni maksimir.domovina.ai s ?lanac=chiado. Preview URL-ovi ne
dolaze u obzir: Certilia proxy ih ne pušta (ALLOWED_ORIGINS), a passkey s rpId = domovina.ai
radi samo na *.domovina.ai.
Prije testa:
- [ ] backup
localStorageu Braveu (maksimir-zk-identity,maksimir-keystore-v1,maksimir-draft,maksimir-auth) u datoteku izvan repoa; - [ ] testni ključ, ne Matijin pravi: isti ključ na Chiadu i Gnosisu daje isti nullifier, pa bi testni listić na Chiadu bio javno povezan s pravim listićem na Gnosisu. Ponovna upotreba ključa iz faze 1 testira se lokalno (lokalni Supabase + Hardhat) s testnim ključem faze 1;
- [ ] relayer Chiado
/statuspokazuje saldo sponzora.
Koraci (passkey korake klikne Matija, jer WebAuthn traži fokus prozora):
| # | Korak | Očekivano |
|---|---|---|
| 1 | prijava eOsobnom | sesija; maksimir_my_ballot() ok |
| 2 | uvjeti v2 | consented_at |
| 3 | novi ključ → 24 riječi → potvrda 3 riječi → passkey | otisak ključa isti na stranici i u imenu passkeyja |
| 4 | registracija | red u maksimir_chain_registrations (Chiado), Registered na lancu; nema zapisa „prijenos” (Chiado nije produkcijski) |
| 5 | drugi zahtjev s drugim commitmentom | already_registered |
| 6 | listić 50/30/20 → predaja | BallotCast, results() se slaže sa stranicom |
| 7 | izmjena s drugog preglednika (24 riječi) | revizija 2; prvi preglednik nakon osvježenja vidi reviziju 2 |
| 8 | „preuzmi paket” + slanje iz skripte bez relayera | uspjeh |
| 9 | javna objava s imenom | kartica s listićem s lanca; OG kartica; zatim isključi |
| 10 | anonimna objava | AnonymousShare; provjera na stranici objave |
| 11 | maksimir_verify.py --chain (manifest Chiado) |
SVE PROVJERE PROŠLE |
Nakon testa: javne objave isključene, localStorage vraćen iz backupa i provjeren (otisak
Matijina ZK ključa isti kao prije), testni passkeyji „Maksimir …” obrisani (Matija).
Na Gnosisu (korak 8 redoslijeda) isti tok ide jednom, s Matijinim pravim ključem iz faze 1 i njegovim pravim glasom. Tako se provjerava i ponovna upotreba ključa na produkciji.
# Plan implementacije (nakon odobrenja)
Svaki korak je jedan ili više commitova na grani, s testovima prije commita.
| # | Korak | Testovi | Checkpoint |
|---|---|---|---|
| I1 | migracija maksimir_chain (tablice, chain_from, keystore RPC-evi, pravilo registracije u cast_ballot, zapis „prijenos”) |
SQL test u domovina-api/supabase/tests/: jedinstvenost, prijenos, zastavica, povratak razine 4, keystore samo dodaje |
commit u domovina-api (bez primjene na prod) |
| I2 | edge funkcija maksimir-register |
Deno test; potpis prolazi register na lokalnom Hardhatu (test u chain/test/) |
commit |
| I3 | relayer: okruženja Chiado/Gnosis, ALLOWED_ORIGINS |
postojeći testovi; /status na Chiadu |
commit + deploy Chiado |
| I4 | web/src/chainConfig.ts, chainVote.ts |
unit testovi toka s lažnim relayerom i Hardhatom | commit |
| I5 | chainVoteView.ts + grananje u glasanjeView.ts |
glasanje-ui.mjs proširen (presretnuti API i RPC), npm run check |
commit |
| I6 | objave s lanca (set_public s nullifierom, share.ts, shareView.ts) |
SQL test + unit provjere dokaza vlasništva (i podmetnutog tuđeg nullifiera) | commit |
| I7 | snapshot v3 + maksimir_verify.py --chain |
fiksture s Chiada, test u suprotnom smjeru | commit |
| I8 | docs.ts, glasanje-kako-radi.md, uvjeti v2 |
npm run check |
commit |
| I9 | E2E Chiado → Gnosis deploy → E2E Gnosis → dan D | kontrolne liste gore | tag glasanje-v1-gnosis, PR u main uz Matijin ok |
Svaka izmjena u chain/ prolazi npx hardhat test, npm run coverage, npm run coverage:client,
npm run typecheck:client i npm run check-frozen (100 % ostaje). Teški procesi idu jedan po
jedan (Mac nema swap).
# Odluke (Matija, 26. 9. 2026.)
| # | Pitanje | Odluka |
|---|---|---|
| 1 | Gnosis deploy nakon E2E na Chiadu | da |
| 2 | Dan D što prije, bez promocije do tada | da |
| 3 | Prijenos listića faze 1 pri registraciji | da, rizik „ni ovdje ni ondje” prihvaćen (ublažen nalazom I-07) |
| 4 | Ostatak faze 1 broji se do kraja | da; kompatibilnost unatrag nije potrebna (na produkciji nema listića) |
| 5 | Anonimna objava na lancu: kratka poveznica u bazi | da |
| 6 | ts_ms se ne zaokružuje |
da |
| 7 | Relayer šalje odmah, bez serija | da |
| 8 | Safe 2/3, solc 0.8.28 | da; na Chiadu isprobano s 3 lokalna EOA-a (03) |
| 9 | Tuđi rad na glasanjeView.ts |
nema ga |
| 10 | Relayer kao podruta istog Workera | da (jedan, atomičan deploy) |
| 11 | Brisanje starog worktreea ../stadion-maksimir-onchain |
obrisan (.env.chiado prije toga kopiran i uspoređen) |
# Stanje implementacije
| # | Korak | Gdje | Provjera | Commit |
|---|---|---|---|---|
| I1 | migracija 20260926120000_maksimir_chain.sql |
domovina-api |
supabase/tests/20260926_maksimir_chain.sql (15 provjera); stari testovi faze 1 prolaze |
f6ad74d, 106f205 |
| I2 | edge funkcija maksimir-register |
domovina-api |
7 Deno testova + fiksni vektor potpisa (isti test u chain/test/client) |
6b6ad33 |
| — | klijent: dokaz vlasništva, čitanje s lanca, korijeni grupe | chain/client/ |
99 testova, chain/client i ugovor 100 %, check-frozen |
178a2c2 |
| I3 | relayer u Workeru | web/worker/relayer/ |
8 testova; scripts/e2e-chiado.ts na pravom Chiadu |
ef4c5d8 |
| I4–I6 | web: tok, rezultati, objave, OG kartica | web/src/chainVote*.ts |
scripts/glasanje-chain-e2e.mjs: 33/33 (pravi Chiado); npm run check; glasanje-ui.mjs |
7f83aaf |
| I7 | snapshot v3 + maksimir_verify.py --chain |
scripts/ |
9 testova bez mreže + živi Chiado; checkpoint → verify od kraja do kraja | 382d046 |
| 8 | vlasnik V1 na Chiadu = Safe 2/3 | chain/scripts/safe-chiado.ts |
1 potpis odbijen, stari vlasnik odbijen, 2 potpisa rade | eafde60 |
Nalazi iz integracije (I-01 … I-08) su u dnevniku nalaza.
# Što E2E provjerava (33 provjere, pravi ugovor na Chiadu)
flowchart LR
A1["A1 listić faze 1<br/>→ prijenos na lanac<br/>(novi ključ, 24 riječi, passkey)"] --> A2["A2 izmjena<br/>(passkey)"]
A2 --> A3["A3 javna objava<br/>+ podmetnut nullifier"]
A3 --> A4["A4 anonimna<br/>objava"]
A4 --> A5["A5 povlačenje"]
A5 --> B["B novi uređaj:<br/>24 riječi"]
B --> C["C ZK ključ faze 1<br/>= isti commitment"]
C --> D["D zbroj na stranici<br/>= lanac + faza 1"]
# Lokalni stack
| Dio | Naredba | Port |
|---|---|---|
| baza | domovina-api$ supabase start; migracija ručno: psql … -1 -f supabase/migrations/20260926120000_maksimir_chain.sql |
55321/55322 |
| registrar | domovina-api$ SUPABASE_URL=… SUPABASE_ANON_KEY=… SUPABASE_SERVICE_ROLE_KEY=… MAKSIMIR_REGISTRAR_KEY_10200=<iz chain/.env.chiado> deno run --allow-net --allow-env --allow-read supabase/functions/maksimir-register/index.ts |
8000 |
| relayer + web build | web$ npm run build && npx wrangler dev --port 8787 (tajna u web/.dev.vars: SPONSOR_PRIVATE_KEY_10200) |
8787 |
| web (razvoj) | web$ npx vite --port 5173 --mode e2e (web/.env.e2e.local: lokalni Supabase + VITE_MAKSIMIR_REGISTER_URL) |
5173 |
| E2E | web$ SUPABASE_SERVICE_ROLE_KEY=… node scripts/glasanje-chain-e2e.mjs [--headed] |
— |
.env.e2e.local, a ne .env.local: Vite .env.local učitava i u produkcijskom buildu (I-06).
Na produkcijskoj domeni tok se isprobava s ?lanac=chiado (samo kad u bazi postoji redak chiado).
# Put do produkcije
Redoslijed, svaki korak uz Matijin ok. Web je siguran i prije koraka 1 (I-04). Matija, 26. 9.: najprije Gnosis (korak 5), zatim offchain dio (registrar, web); migracija baze može odmah.
- ✅ Baza (26. 9. 2026., 23:59 UTC):
20260926120000_maksimir_chain.sqlprimijenjena ručno, u jednoj transakciji s upisom uschema_migrations(tuđe20260903*nisu dirane). Backup:domovina-api/backups/pre-maksimir-chain-20260925-235856-{schema,maksimir-data}.sql. Provjera: faza 1 netaknuta (0 listića, lanac seq 2, ZK seq 1, otvorena),maksimir_chain_configprazan,maksimir_logi dalje zatvoren,npm run checkna produkciji prošao. Redakchiadojoš nije dodan (dolazi s deployem weba). Zamka:scripts/lib/db-env.shdetect_db_containerpojede stdin (ssh bez-n), pa se uz--stdinzadajeCOOLIFY_DB_CONTAINER. - ✅ Registrar (26. 9. 2026.):
maksimir-registerdeployana (scripts/deploy-functions.sh --only=maksimir-register -y), tajneMAKSIMIR_REGISTRAR_KEY_100i_10200(coolify-env-set.sh … --recreate-service=supabase-edge-functions); bez sesije vraća 401not_signed_in,certilianepromijenjena. Retcignosis(counts) ichiado(test) umaksimir_chains. - ✅ Web + relayer (26. 9. 2026., Worker verzija
10ee0cf4):RELAYER_CHAINSs obje mreže, tajneSPONSOR_PRIVATE_KEY_100i_10200; sponzor Gnosisa dobio 0,25 xDAI od deployera (tx).npm run checkna produkciji prošao; bez?lanac=web je i dalje faza 1. - E2E s pravom eOsobnom na Chiadu (Brave, Matija klikne passkey) po kontrolnoj listi.
- ✅ Gnosis (26. 9. 2026.): Safe 2/3
0xfb1b…c576, V10x812960FA1120121DEd82A8806aECE93dcf49E869(Sourcify exact_match, tagglasanje-v1-gnosis, 03). Preostaje za ovaj korak: redakgnosisscounts = trueumaksimir_chains;RELAYER_CHAINS+SPONSOR_PRIVATE_KEY_100; tajnaMAKSIMIR_REGISTRAR_KEY_100u edge funkciji. - ✅ E2E na Gnosisu (26. 9. 2026., Matija, produkcija,
?lanac=gnosis, prava eOsobna, passkey): prvi pokušaj otkrio je I-09 (ključ od nula) → zaštite + novi V1; drugi pokušaj s pravim ključem iz faze 1 (24 riječi, commitment157304732939…) prošao: tx,ballotOf= listić (100 bodova, 5 radova), 1 registracija, 1 glasač. - ✅ Dan D (26. 9. 2026., Matijin ok; izveo Claude Opus 5.5):
- Prije:
npm run checkna produkciji prošao; backupdomovina-api/backups/pre-dan-d-20260926-011924-{schema,maksimir-data}.sql. Stanje: lanac faze 1seq 2(listić 25. 9. 13:36 UTC i povlačenje 22:15 UTC, bez zapisa „prijenos”), 0 aktivnih listića, ZK grupa 1 član, na Gnosisu 1 registracija i 1 glasač. - Zastavica:
update maksimir_settings set active_chain_id = 100, active_contract = '0x8129…e869', chain_from = now()u jednoj transakciji s assertima prijecommit(dva pokušaja su se sama poništila zbog krivih pretpostavki u assertima, ne u bazi).chain_from = 2026-09-26 01:20:41.770077+00. Provjereno u transakciji i zatim izvana kaoanon:maksimir_results()phase1_open = false,chain_open = true;maksimir_chain_config().active= Gnosis V1 (groupId 249, blok deploya 48440122);_maksimir_cast_ballot_for(verificiran korisnik),maksimir_zk_register,maksimir_zk_share→voting_closed;maksimir_log()javan (2 retka). - Završni OTS snapshot: run 36208126330,
commit
dfab6ff,glasanje/checkpoints/20260926T012117Z-seq2.json:maksimir-snapshot/3,phase1_final: true, vrh faze 1b3b94377…9de16, ZK vrh433a3c8a…a48e,chain= Gnosis blok 48440478 (0x0ae8fdf5…cbe3), 1 registracija, 1 glasač. - Izvoz:
glasanje/faza1/lanac.jsonizk_grupa.json(92982e9).maksimir_verify.py … --chain chain/deployments/gnosis/v1.json→ SVE PROVJERE PROŠLE (6 snapshotova, ZK grupa, zbroj iz događaja =results(),blockHashsnapshota = lanac; ukupno 1 glasač). Usput popravljeno: skripta je pucala nacheckpoints/index.jsonkoji hvata glob iz dokumentacije; sada ga preskače (test + mutacija). - Web nakon prijelaza (bez novog deploya):
npm run checkna produkciji prošao; u Braveu, bez?lanac=,/glasanjeprikazuje tok na lancu („Glas na lancu · revizija 1”, anonimna objava na lancu), 1 glasača, bez trake „testna mreža”;/radovi/NONDB5MRFprikazuje rezultat s lanca (24 %, 1 podupiratelj). Nijedna greška u konzoli; ništa nije kliknuto ni predano. - Povratak (ako zatreba): razina 4 iz Povratak,
update maksimir_settings set chain_from = null.
- Prije:
# Otvoreno nakon sesije 26. 9. 2026.
Gnosis: deployano 26. 9. 2026. (vidi korak 5 gore). Deployer 0x880e…891f ima ostatak ~0,3 xDAI
(Matija je uplatio 0,3): plan je ostatak prebaciti sponzoru relayera 0x7789…6549 kad relayer za
Gnosis ide u produkciju (odluka Matije). Sljedeće po Matijinom redoslijedu: offchain dio
(registrar maksimir-register + tajne, redak gnosis u bazi, web + relayer), pa E2E na Gnosisu.
Neovisni review (drugi pregled, druga sesija,
Fable 5.1): F-22 (CI na grani) popravljen u f9b2dfe. Otvoreni i treba ih riješiti ili izrijekom
prihvatiti prije dana D: F-01/F-13 (povezivanje po vremenu: registracija, zapis „prijenos” i
prvi listić u istom trenutku), F-02/F-14/F-16 (anonimne točke pisanja bez provjere SNARK-a), F-04
(snarkArtifacts nisu prikvačeni), F-06 (keystore), F-15 (registracija bez dokaza posjeda tajne),
F-17 (500 s porukom, localhost:5173 u produkcijskom CORS-u), F-18/F-21 (?lanac= smije birati i
ugovor koji se broji, prije dana D), F-20 (veza commitment ↔ nullifier u localStorage i
__chainVote), F-21 (checkpoint pada kad RPC Gnosisa ne odgovori). Redoslijed koji review predlaže
je u njegovu sažetku.
Odluka za dan D (Matija, 26. 9. 2026.): otvoreni nalazi reviewa ne blokiraju dan D i izrijekom
su prihvaćeni kao poznati rizik do popravka. Nijedan ne dira integritet zbroja (to je review i sam
potvrdio); tiču se privatnosti prema operateru (F-01, F-13, F-06, F-20), anonimnih točaka pisanja
(F-02, F-14, F-16), lanca opskrbe ZK artefakata (F-04), griefinga registracije (F-15) i rubova relayera
(F-17: 500 i dalje vraća poruku viema, workers_dev/preview_urls uključeni). F-18/F-21 (?lanac= prije
dana D) nakon prijelaza više nisu relevantni; F-21 (checkpoint i pad RPC-a) popravljen je u b2a9d0f,
F-22 u f9b2dfe. Stanje na dan D: prema F-13, zapis „prijenos” nije nastao ni za koga (Matijin listić
faze 1 povučen je prije registracije), pa javni lanac faze 1 nikoga ne povezuje s lancem. Satni cron
(F-05) i dalje preskače runove; rezervni okidač nije napravljen.
Ostalo: obje grane mergane u main (PR #2 49c362c, domovina-api PR #4 346fd63). Testni passkey
„Maksimir glasanje · ključ …” za localhost može se obrisati iz Lozinki. Ostatak xDAI-ja deployera može
ići sponzoru relayera Gnosisa kad zatreba.