Skip to content

Commit 9599771

Browse files
stackdumpclaude
andcommitted
Phase B / B5.8b: createPoll v3 branch in poll.js
window.createPoll reads a 'poll-max-privacy' checkbox and, when set, generates sk_creator client-side, derives pk = G·sk, persists sk to localStorage, downloads the JSON backup, and stamps voteSchemaVersion=3 + pkCreator on the create-poll request body. The toggle element itself is still B5.9 work; this slice safely no-ops when the element is absent so the existing v2 flow continues to be the default. Order of operations is intentional: sk is generated and pk derived before any network call (a server failure leaves no orphan pkCreator), and the localStorage save fires before the backup download (a dismissed download dialog still leaves a recoverable key in the browser). UI message after a v3 create explicitly warns the user that losing both localStorage and the backup file means the poll cannot be closed — the structural single-creator-key risk surfaced before they navigate away. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f7b7f10 commit 9599771

1 file changed

Lines changed: 80 additions & 11 deletions

File tree

public/poll.js

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { MerkleTree } from './merkle.js';
55
import { buildVoteCastWitness, buildVoteCastHomomorphicWitness, HOMOMORPHIC_CHOICES } from './witness-builder.js';
66
import { prove as workerProve, loadKeys, initProver } from './prover.js';
77
import { SUBGROUP_ORDER as PEDERSEN_SUBGROUP_ORDER } from './pedersen.js';
8+
import {
9+
generateSkCreator,
10+
derivePkCreator,
11+
saveSkCreator,
12+
downloadSkBackup,
13+
} from './sk-creator-store.js';
814

915
// Current poll context
1016
window.currentPollId = null;
@@ -154,10 +160,44 @@ window.createPoll = async function() {
154160
if (!title) return showMsg('Title is required', 'error');
155161
if (choices.length < 2) return showMsg('At least 2 choices required', 'error');
156162

163+
// Read the v3 (homomorphic-tally) toggle. Element doesn't exist
164+
// until B5.9 lands; gracefully default to false so this slice
165+
// works against the current HTML.
166+
const v3Toggle = document.getElementById('poll-max-privacy');
167+
const useV3 = !!(v3Toggle && v3Toggle.checked);
168+
if (useV3 && choices.length > HOMOMORPHIC_CHOICES) {
169+
return showMsg(
170+
`Maximum-privacy polls support up to ${HOMOMORPHIC_CHOICES} choices (this poll has ${choices.length})`,
171+
'error',
172+
);
173+
}
174+
157175
const btn = document.getElementById('btn-create');
158176
btn.disabled = true;
159177
btn.textContent = 'Connecting wallet...';
160178

179+
// sk_creator is generated *before* any network call so a creation
180+
// failure can't leave the server with a pkCreator that no client
181+
// possesses the matching sk for. Saved to localStorage at the same
182+
// moment the backup downloads — the user has two recovery paths
183+
// (browser storage + downloaded file) before the poll exists on
184+
// the server.
185+
let v3Sk = null;
186+
let v3PkHex = null;
187+
if (useV3) {
188+
try {
189+
v3Sk = generateSkCreator();
190+
v3PkHex = derivePkCreator(v3Sk);
191+
} catch (err) {
192+
btn.disabled = false;
193+
btn.textContent = 'Create Poll';
194+
return showMsg(
195+
'Failed to generate poll creator key: ' + (err.message || err),
196+
'error',
197+
);
198+
}
199+
}
200+
161201
try {
162202
// Require wallet signature
163203
const provider = getWalletProvider();
@@ -179,19 +219,24 @@ window.createPoll = async function() {
179219
});
180220

181221
btn.textContent = 'Creating...';
222+
const body = {
223+
title,
224+
description,
225+
choices,
226+
durationMinutes: duration,
227+
voterCommitments: [],
228+
registryRoot: '',
229+
creator,
230+
signature,
231+
};
232+
if (useV3) {
233+
body.voteSchemaVersion = 3;
234+
body.pkCreator = v3PkHex;
235+
}
182236
const resp = await fetch('/api/polls', {
183237
method: 'POST',
184238
headers: { 'Content-Type': 'application/json' },
185-
body: JSON.stringify({
186-
title,
187-
description,
188-
choices,
189-
durationMinutes: duration,
190-
voterCommitments: [],
191-
registryRoot: '',
192-
creator,
193-
signature,
194-
})
239+
body: JSON.stringify(body),
195240
});
196241

197242
if (!resp.ok) {
@@ -200,7 +245,31 @@ window.createPoll = async function() {
200245
}
201246

202247
const data = await resp.json();
203-
showMsg('Poll created! Share this link with voters.', 'success');
248+
249+
// v3: persist sk + trigger backup download. Persist FIRST so a
250+
// download dialog that the user dismisses still leaves a key on
251+
// disk; backup is the secondary recovery path.
252+
if (useV3) {
253+
try {
254+
saveSkCreator(data.id, v3Sk);
255+
} catch (err) {
256+
console.warn('saveSkCreator failed', err);
257+
}
258+
try {
259+
downloadSkBackup(data.id, v3Sk, v3PkHex, {
260+
pollTitle: title,
261+
creator,
262+
});
263+
} catch (err) {
264+
console.warn('sk backup download failed', err);
265+
}
266+
showMsg(
267+
'Maximum-privacy poll created! Your creator key was saved to this browser and a backup file was downloaded. Lose both and the poll cannot be closed.',
268+
'success',
269+
);
270+
} else {
271+
showMsg('Poll created! Share this link with voters.', 'success');
272+
}
204273

205274
// Show the poll link
206275
const msgDiv = document.getElementById('messages');

0 commit comments

Comments
 (0)