Repository navigation
Final review before the Amoy deploy: hidden re-votes, a tally proved on chain, security and quality - #1
Merged
Conversation
Paillier lets the key holder recover a ciphertext's randomness, so a tally can be opened: the randomness that opens the sum of the valid final ballots to exactly the published counters, and a public opening of each ballot excluded as invalid. Dependency free, so the browser, the auditor's CLI and the contract tests share one definition of a valid tally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
- ElectionV4 no longer inherits ERC2771Context: the deploy script fell back to the deployer as trusted forwarder, which could speak as any organizer. - Ballots capped at 512 bytes; voteEnd is exclusive everywhere. - publishResults takes the tally proof and the excluded-ballot count, checks the counters account for distinctVoters, and emits TallyProofPublished. - markVoided is refused on a non-cancellable election whose result is publishable: voiding it would be the veto it promised not to have. - enrollPrivate carries a documentTag on gated elections, so one passport behind several World ID accounts enrols once. - ElectionPaymaster: factory set once, relay params bounded, even the factory limited to an organizer's own elections, sponsored re-votes spaced by a one-hour cooldown so no voter can drain a tank. - Shared TwoStepOwnable; PlatformRegistry caps vault entries and sizes. - Seed and E2E publish real proofs; seed tag derivation matches the backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
…ed writes - The World ID action is fixed by the server and checked on the proof: a client-chosen action minted a new nullifier, and identity, per action. - Private enrolment signs a document tag and consumes the passport check only once the enrolment has landed. - Credentials no longer assert an unverified country, region or age, and first-time sessions are revocable through the human's slot. - Per-voter quotas and size limits on every platform-paid registry write. - One provider and one serialized signer per key (no nonce collisions); one generic relay path; errors reduced to the revert name for clients. - Revocation list read in parallel, deduplicated, never truncated. - Security headers, JSON error handler, body limit, oxlint, noUnused*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
- Results are checked in the reader's browser against the chain (TallyCheck), the organizer's tally publishes its proof, and the quorum is read from the contract. - The vote confirmation never shows a made-up reference number. - Per-election identities are cached per master identity, and a failed RPC no longer caches an election as publicly enrolling. - Recovery phrase derivation is stretched (PBKDF2, 600k rounds). - Wallets that sign non-deterministically get an exportable random tally key instead of one that could never be derived again. - Paginated election list and bounded concurrency for history and receipts. - oxlint; render-loop-safe state; Playwright smoke test works standalone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
Twelve words from 128 carried 84 bits. The EFF Short Wordlist 1 keeps the same design goals (short, common, easy to type, no homophones) with 1295 words, so twelve now carry about 124 bits, drawn by rejection sampling so no word is likelier than another. Also runs the checks on pull requests into dev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
The move to the EFF list left three suites holding phrases from the old one, which identityFromPhrase now rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
…d verifiers The ballot circuit proves a ballot is exactly one option, encrypted with exponential ElGamal on Baby Jubjub, and that its cancellation undoes the voter's OWN previous ballot, found by membership in the ballots tree rather than by pointing at it. Tags per chain step and per epoch are derived from the voter's secret, so nothing public links two ballots of one voter. The tally circuit proves the counts are the decryption of the election's aggregate, or, in its other mode, only that fewer voted than the quorum. build.mjs compiles both per size, runs the setup (a public development ceremony unless CEREMONY_ENTROPY is set; PTAU takes a public phase 1), caches, and writes the Solidity verifiers and the browser's proving files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
ElectionV4 takes ballots with a Groth16 proof, files each under an unlinkable tag in a ballots tree, keeps the running ElGamal aggregate of votes and cancellations, and publishes a result only with the tally circuit's proof of its decryption. voidBelowQuorum ends a small election without revealing its counts. Epoch tags replace the per-nullifier re-vote cooldown, which was itself a public link between a voter's ballots. The tag scope derives from the contract's own address. Root validity is timed from when a root was superseded, as Semaphore does: timed from its creation, a root current for over an hour expired the moment another ballot landed, stranding in-flight proofs in quiet elections. ElectionDeployer holds the creation code, which no longer fits in the factory; the factory picks the smallest verifier pair per option count. deploy.ts deploys the generated verifiers and refuses a development ceremony off the local chain. The E2E suite proves for real, and a new ABI parity test checks every hand-written fragment in the frontend, backend and CLI against the artifacts, which found relayEnrollPrivate's local-relay signature still missing documentTag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
The browser walks the voter's own chain of tags, builds the next ballot with a cancellation of the last one and proves it with snarkjs; the organizer decrypts the on-chain aggregate with wallet-derived keys and proves the tally; readers re-add every BallotCast to check the aggregate. Receipts and history go by ballot tag. The relay validates the new ballot shape before simulating. The CLI proves and publishes (or voids below the quorum) and --verify re-checks the published proof against the verifying key; its outcome rules now match the contract's. Fixes the local relay's relayEnrollPrivate ABI, which lacked documentTag. The privacy texts no longer claim the organizer cannot read a single ballot: the app only decrypts totals, but the key could open one ballot, never whose it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
CI gains a circuits job (witness tests, cached build seeded from the Hermez phase 1) whose outputs the contracts job needs, with REQUIRE_CIRCUITS so a missing build fails rather than skips. README, architecture, glossary and the module guides describe the new scheme and its limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
…zed epoch refusal Found by running the whole stack against a local node: the seed's finished elections gave 90 seconds of voting to ballots that now take seconds each to prove; the CLI answered --verify and then hung on snarkjs's worker threads; and a second ballot within the hour, refused in the browser before proving, reached the voter as an English error string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
The suite skipped itself when no build existed, and CI runs it before the build: it would have passed on every push while testing nothing. npm test now compiles the smallest size first (build.mjs --compile, seconds, no setup), and a missing build is an error rather than a skip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
The Hermez phase 1 now answers 403, so the runner generated its own 2^17 phase 1: 38 minutes, and the 40-minute build step timed out right after it, saving nothing. build.mjs --ptau <power> generates only that file; CI caches it under its own key, saved as soon as it exists, so the cost is paid once and later runs go straight to the minutes-long phase 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
The development-ceremony check ran after Poseidon, the registry, the domains and the paymaster were already deployed, and the missing-attester check after the verifiers too. On Amoy a refused deployment had paid for up to five contracts and left them orphaned. Both checks, and the missing-circuits one, now run before anything is sent. Checked against a local node posing as chain 80002: with the development ceremony it refuses at nonce 0; with ALLOW_INSECURE_CEREMONY=1 the whole non-local path deploys, Poseidon through the CREATE2 proxy included.
circom2, the WebAssembly build, cannot compile these circuits on Windows:
its WASI filesystem does not see two includes of circomlib's poseidon as
one file ("Duplicated callable symbol") and then cannot write its output.
build.mjs also spawned its .bin shim, which is a .cmd there and fails with
ENOENT before any of that.
It now runs a native circom when there is one (CIRCOM, or circom on the
PATH) and circom2 through node otherwise, and on Windows without a native
compiler it says what to install instead of failing obscurely. The native
v2.2.3 release, the version circom2 embeds, builds wasm byte-identical to
CI's; the witness tests pass 8/8 on Windows with it.
"Ballots paid for" divided a reserve by one median across every relayed ballot, and most are five-slot ones. A six-option election proves with the nine-slot circuit, about 1.24M gas against 0.8M, so it was promised 124 ballots its reserve could pay about 79 of. The cost is now measured per circuit size, read from each relayed ballot's own calldata, and every screen about one election uses the size it proves with. A size nobody has voted in yet is scaled up from the largest measured one, never down.
… end to end Found driving the whole flow in Chromium against a local chain: - A dropped RPC read showed "page not found" for an election that exists, and one the RPC never answered left the page on a spinner for good. Reads now time out, a failed one is retried once, and a connection failure gets its own screen with a retry on the election, results, management and change-vote pages. ChangeVote rendered nothing at all. - A second ballot inside the hour said "Something went wrong" without saying when the next one is allowed. It now says the latest ballot already counts and gives the time. - The election page labelled the ballot tag "Reference", the name every other screen gives the transaction hash: two different numbers under one name. - The timeline and sort labels had lost their accents in five languages, and German said "payout" (Auszahlung) where it meant the count (Auszaehlung). - "1 voters" on the verified tally: the count is pluralised now. - Modals had no dialog role and the date picker's hour and minute fields, like the modal's close button, were labelled in English only. - The smoke test visited /voter/election/:id, which no longer exists and passed on the 404 page.
…ections Found running the smoke suite against a configured local chain: - The dev proxy reused a socket the Hardhat node had just closed as idle, and the read came back as ECONNRESET: an election page failed or waited for good about one load in three. The /rpc proxy no longer keeps connections alive. The smoke suite went from 4 failures in 12 runs of the election pages to 72/72 on all three devices. - The demo elections' placeholder addresses (0xCon1aaa) passed the 0x check in useElectionFunding, so ethers took them for ENS names and logged an error on every render. Only a whole address is read now.
…ts options voteCostFor scaled the largest measured size by the election's option count. With only five-slot ballots measured, a six-option election came out at 6/5 of their cost, though it proves with the nine-slot circuit: an underestimate, the opposite of what a figure shown as "ballots paid for" may be. It now works out the circuit the election proves with, from the sizes build.mjs ships.
…poch The contract accepts a ballot naming the current epoch or the last one, each tag once, but the app named only the current one. A voter who had just cast waited for the next clock hour, and a vote forced after the last hour boundary before the close could not be replaced at all. chooseEpoch now uses both. When both are free it draws one at random: the epoch is public calldata, and naming the previous one only once the current one was spent would mark the ballot as a re-vote, in the last hour, where a coercer watches. Inside two minutes of the turn the previous epoch is skipped, since the proof would arrive stale.
…s do - Terms said one gas tank pays for all of an organizer's elections and that voting stops in every one when it empties. Since the per-election reserve (2026-09-16) each election pays from its own reserve first, then from the shared free balance, and only that election stops. - "Change your vote whenever you want" was never true under the hourly limit. How it works, privacy and the limit messages now say it: straight away once, then once an hour, and nobody watching can tell. - Privacy states the limit that remains: two votes forced in a row in the last hour before the close cannot be replaced. - Page dates follow their content.
…our limit - README: anonymity comes from Votain's own ballot proof over Semaphore identities, not from Semaphore proofs; test counts; the last-hour limit. - Glossary: nullifier and nonce described the scheme the hidden re-votes replaced; adds the epoch tag, fixes the V4 identity commitment. - deployment.md had no step for the contracts: ceremony, attester, deploy:amoy, the manifests to commit, VITE_CIRCUITS_URL, backend addresses. - PLAN.md and the agent log presented Paillier as current; marked as history. - Package guides: test counts, deploy:amoy's ceremony requirement, and backend debt that no longer applied. - deploy.ts: deployVerifiers' comment back above deployVerifiers.
npm-check-updates -u in contracts, backend, frontend and scripts-tally; circuits was already current. One major: hardhat-toolbox-mocha-ethers 3 to 4, which moves the contract tests to Mocha 12 and hardhat-mocha 4. The rest are minors and patches (hardhat 3.18, idkit 4.3, vite 8.3.1, vitest 5.0.2, oxlint 1.86, snarkjs 0.7.6, dotenv 18.0.4). After npm install, all green: contracts 222 (the E2E suite with real Groth16 proofs), backend 160, frontend 597, lint, typechecks and builds. oxlint 1.86 adds React's purity rule, which caught the date picker reading the clock during render to mark today. It now reads it when the popover opens.
poseidon-lite's root index is CommonJS and loads the round constants of all sixteen arities, which no bundler can tree-shake: the vendor chunk weighed 581 kB for the three arities the app uses, the one chunk over Vite's 500 kB warning. Importing poseidon2/3/4 from their own subpaths, as Semaphore and zk-kit already do, brings it to 61 kB. No limit or chunking rule changed.
- Root: the system context (phone apps, frontend, issuer in its TEE, the contracts, the tally CLI) and a vote end to end, from sign-in to a result anyone can re-check. Replaces the ASCII sketch. - frontend: pages, hooks and the lib modules, down to snarkjs, the issuer and the RPC. - backend: routes, the modules behind them, and the two keys that reach the chain through one signer queue. - contracts: who deploys, calls and verifies whom. - circuits (new README): from the circom sources to the verifiers, the proving files and the deployment. - scripts-tally (new README): the organizer's tally and anyone's --verify. Every diagram renders with Mermaid 11. The package READMEs also lost what the hidden re-votes made untrue: a per-nullifier nonce, old test counts.
… epochs The README's coercion row and the glossary still said one ballot per voter per hour. Since the app names the previous epoch too, a voter can replace a ballot straight away once, then one an hour; the glossary also says why the choice between the two epochs is random.
- architecture.md: the overview and the full voting flow, now a flowchart and a sequence diagram with the reasoning kept as text beneath. The ASCII versions had drifted: an /attestation endpoint the backend does not serve (the TDX report comes from Phala's dstack), enrolment of the platform commitment where elections now take one derived per election, and one ballot an hour. The election state machine is a state diagram. - deployment.md: the pipeline showed three checks; CI runs five, with circuits feeding contracts, and publishes a release. - backend/DEVELOPMENT.md: the World ID sign-in, as a sequence diagram. Directory trees stay as text. All diagrams render with Mermaid 11.
Threshold decryption and an auto-tally stay out of this version, by decision: a Future work section collects them, with the tally deadline, the last-hour coercion fix and the scaling work, and points to the design sections that already argue each one. Adds the limit nobody had written down: there is no publishing deadline, so an organizer who never decrypts leaves the election in TALLYING for good, and can release its gas reserve once voting has ended.
The registrar pays, unreimbursed, for every registration, vault entry, preferences blob, rotation and revocation: a cost that grows with users. None of it is on the voting path now that enrollPrivate never reads the registry. Future work describes batching it behind one Merkle root per period, with the blobs in content-addressed storage, or billing a per-voter platform fee to the organizer's reserve.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumen
Revisión completa del proyecto (contratos, backend, frontend, CLI de recuento y CI) antes de desplegar en Polygon Amoy, corrección de todo lo encontrado que tenía arreglo en código y, sobre eso, re-votos invisibles: nadie que observe la cadena puede saber que un votante ha cambiado su voto. Lo más importante:
Verificación (checkout limpio del último commit, como en CI):
Además, contra un nodo Hardhat local:
tally --verifyverifica todas las publicadas desde la cadena, sin clave: vuelve a sumar las papeletas y vuelve a comprobar la prueba contra la clave de verificación.🕶️ Re-votos invisibles
Antes:
VoteCastpublicaba elnullifierdel votante y unnonce, así que quien conociera el nullifier veía que había re-votado. A un coaccionador le basta con eso.Ahora (
circuits/src/ballot.circom,ElectionV4.castVote):A = r·G,B_i = v_i·G + r·H_i, una clave por opción;A' = −A_prev + s·G,B'_i = −B_prev_i + s·H_i.tag = Poseidon(TAG, secreto, scope, k)es suyo, conkprivado: cada paso de la cadena del votante se emite una vez y los pasos no se pueden enlazar.BabyJubJub.sol). La suma se telescopa al último voto de cada votante: no hay que deduplicar nada.epochTagpor votante y epoch de una hora sustituye al enfriamiento por nullifier del paymaster, que era en sí mismo un vínculo público entre las papeletas de un votante. La prueba puede nombrar el epoch actual o el anterior, cada tag una vez: un reemplazo inmediato y después uno por hora. La app elige al azar entre los dos cuando ambos están libres, para que nombrar el anterior no delate un re-voto.scopede los tags sale de la dirección del propio contrato, no del organizador: dos elecciones nunca comparten tags.circuits/src/tally.circom): prueba que los contadores son el descifrado del agregado del contrato con sus claves.publishResults(cid, counts, proof)lo verifica on-chain.voidBelowQuorum(voters, proof)prueba solo que votaron menos que el quórum y anula sin revelar nunca los contadores.build:all). La factory elige el par de verificadores más pequeño que cabe.ElectionDeployerguarda el bytecode de creación deElectionV4, que ya no cabía dentro de la factory por el límite de 24 KB.Lo que ve cada uno:
Coste medido por la suite E2E: ~0,99M gas por papeleta relayada con 5 slots y ~1,3M con 9. El tope por relay del paymaster sube a 5M (techo de 6M), porque se cobra el gas medido y un tope por debajo haría pagar la diferencia al relayer.
🔴 Seguridad
ElectionV4heredabaERC2771Contextydeploy.tsusaba la dirección del deployer como forwarder si faltabaTRUSTED_FORWARDERERC2771Contexteliminado por completo: nada lo usaba/worldid/requestfirmaba conDEVELOPER_KEYlaactionque elegía el clienteactionda un nullifier distinto: una persona obtenía identidades ilimitadas (Sybil)action(worldIdAction()) yverifyWorldIdProofrechaza pruebas de otradocumentTagenenrollPrivate, derivado del documento con la clave de época y deduplicado on-chainrelayVotesin permisos, re-votos ilimitados y papeletas de tamaño arbitrarioepochTag: un reemplazo inmediato y después uno por hora. El voto coaccionado se puede sobrescribir, salvo dos votos forzados seguidos en la última hora antes del cierre (documentado como límite conocido)reserveFromBalanceForlimitado a las elecciones del propio organizadormarkVoidedsin restriccionescancellable == false, solo sin prueba si hay menos papeletas que el quórum; si no, solo convoidBelowQuorumy su pruebaageOver18: true, país y región a todo el mundostatusListIndex: "0"verifySessioncomprueba la revocación por el hueco del humano✅ Recuento verificable
TallyChecken la pantalla de resultados vuelve a sumar todas lasBallotCasty comprueba que dan el agregado que se probó;npm run tally -- <elección> --verifyhace lo mismo y además vuelve a verificar la prueba publicada contra la clave de verificación del circuito, independientemente del verificador on-chain.tallyProof.ts) de la primera fase de esta PR, que dejaba publicar un recuento y luego contradecirlo; ahora no se puede publicar.🟡 Bugs
VoteConfirmation: mostraba un número de referencia inventado (VTN-2025-xxxxx) al recargar la página. Ahora nunca inventa uno.voting.ts: un fallo de RPC dejaba cacheada la elección como de inscripción pública, y se votaba con la identidad equivocada.electionIdentity.ts: la caché se indexaba solo por elección. Tras una recuperación seguía devolviendo identidades de la identidad maestra anterior.registrar.ts: daba por registrado un commitment que pertenecía a otro votante.voteEndpasa a ser exclusivo en todas partes, igual que enphase().ElectionPaymaster: citaba una funcióndepositForque no existe.Encontrados en esta fase, al ejecutar todo el sistema
relayEnrollPrivatesindocumentTag. El cambio dedocumentTagno llegó a la ABI escrita a mano del relay local: en la cadena local la inscripción privada fallaba. Nuevo test de paridad de ABI (AbiParity.test.ts) que comprueba cada fragmento escrito a mano del frontend, backend y CLI contra los artefactos compilados. Lo encontré leyendo el código; el test está comprobado contra la versión con el fallo, que ahora no pasaría.poseidon-solidityy@zk-kit/lean-imt.solllegaban solo a través de@semaphore-protocol/contracts. Ahora son directas.--verify, el seed no dejaba tiempo para probar papeletas reales, y un segundo voto en la misma hora llegaba al votante como texto en inglés.⚙️ Escalabilidad
chain/signer.ts), conNonceManager. Se acabaron las colisiones de nonce con peticiones concurrentes.getElectionspaginado, e historial y recibos en paralelo con un límite de concurrencia.🧹 Calidad y herramientas
ballotCrypto.ts, sin dependencias, lo usan el navegador, el CLI, los tests y el seed.contracts/scripts/lib/prover.tsprueba desde Node exactamente como el navegador. Una sola función de relay,TwoStepOwnablecompartido y un helper de errores.circuits(tests de testigos + build cacheado) que alimenta al job de contratos. La fase 1 de desarrollo (2^17, ~40 min en un runner) se genera una vez y se cachea aparte, guardada en cuanto existe.REQUIRE_CIRCUITS=1hace que un build ausente falle en vez de saltarse la E2E.typescript-eslintno soporta TypeScript 7. Se corrigieron 19 errores reales.enginesy.nvmrc).test:e2eestaba roto porque faltaba@playwright/test. El smoke test ya es autónomo y se ejecuta en CI.paillier-bigint,@semaphore-protocol/proofy@semaphore-protocol/contracts.🔧 Cambios posteriores (27-28 sep)
chooseEpoch), elegido al azar cuando ambos están libres y descartado en los últimos 2 minutos antes del cambio de hora.deploy.ts: las comprobaciones de ceremonia, attester y circuitos se hacen antes de la primera transacción, así que un despliegue rechazado en Amoy ya no deja contratos pagados y huérfanos.build.mjsusa uncircomnativo si lo hay (oCIRCOM), porque circom2 (WASM) no compila estos circuitos en Windows.🚀 Despliegue en Amoy
cd circuits && PTAU=<fase 1 pública de al menos 2^17> CEREMONY_ENTROPY=<secreto> npm run build. La fase 1 debe venir de una ceremonia con muchos participantes (Hermez o la perpetual powers of tau de PSE); la URL histórica de Hermez en Google Storage ya responde 403, así que hay que obtenerla de un espejo y comprobar su hash. Mejor aún, una contribución multiparte en la fase 2. Descartar la entropía después.npm run deploy:amoy. Despliega los verificadores generados y se niega a usar la ceremonia de desarrollo fuera de la cadena local, salvoALLOW_INSECURE_CEREMONY=1para una prueba desechable.frontend/public/circuits/(IPFS, por ejemplo) y poner su URL enVITE_CIRCUITS_URL. Son ~50 MB por zkey, así que no van al repositorio. Un fichero equivocado solo produce pruebas que la cadena rechaza.💡 Propuestas
Descifrado individual (el organizador podría abrir una papeleta suelta)
H_i = Σ_j x_ij·Gsin que nadie tenga la clave completa;x_ij·Adel agregado del contrato con una prueba de igualdad de logaritmos (Chaum-Pedersen) o un circuito pequeño;x_ipor la suma de parciales. Ninguna coalición de menos de t puede abrir papeletas sueltas.x_ise generan y se sellan en el enclave (Phala), que solo descifra y prueba el agregado cuandophase()indique cierre, comprobado por atestación. Es más simple, pero concentra la confianza en el hardware.Indexador
ElectionCreated,MemberEnrolled,BallotCast,ResultsPublishedyVoidedBelowQuorum;eth_call.Estado compartido del backend
Store, para:rate-limit-redis);🤖 Generated with Claude Code
https://claude.ai/code/session_01JD6VzyiKH5aMXbSnv7zNXi
Generated by Claude Code