<!-- Private Set Union demo: two parties compute A ∪ B without revealing their private elements. -->
<div class="parties">
<div class="party" id="alice-panel">
<div class="party-label">Alice’s private set</div>
<div class="party-hint">Click items to toggle. These are Alice’s private elements.</div>
<div class="items" id="alice-items"></div>
</div>
<div class="party" id="bob-panel">
<div class="party-label">Bob’s private set</div>
<div class="party-hint">Click items to toggle. These are Bob’s private elements.</div>
<div class="items" id="bob-items"></div>
</div>
</div>
<div class="btns">
<button id="run" type="button">Run protocol</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div class="result-area" id="result-area" style="display:none">
<div class="result-label">Union result (A ∪ B)</div>
<div class="result-items" id="result-items"></div>
<div class="privacy-note" id="privacy-note"></div>
</div>
/* Layout and color scheme for the PSU demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.parties { display: flex; gap: 1rem; margin-bottom: .8rem; flex-wrap: wrap; }
.party { flex: 1 1 200px; background: #f0f4f8; border-radius: 10px; padding: .8rem 1rem; }
#alice-panel { border-top: 3px solid #1d3557; }
#bob-panel { border-top: 3px solid #457b9d; }
.party-label { font-weight: 700; font-size: 1rem; margin-bottom: .25rem; }
.party-hint { font-size: .82rem; color: #555; margin-bottom: .5rem; }
.items { display: flex; flex-wrap: wrap; gap: .4rem; }
.item { padding: .3rem .65rem; border-radius: 20px; font-size: .88rem; font-weight: 600;
cursor: pointer; border: 2px solid transparent; transition: all .12s; user-select: none; }
.item.alice-off { background: #dce8f5; color: #1d3557; border-color: #aac4e0; }
.item.alice-on { background: #1d3557; color: #fff; border-color: #1d3557; }
.item.bob-off { background: #d6eaf6; color: #457b9d; border-color: #90c0da; }
.item.bob-on { background: #457b9d; color: #fff; border-color: #457b9d; }
.item:hover { opacity: .82; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.err { color: #c92f3c; }
.result-area { background: #eafaea; border-radius: 10px; padding: .8rem 1rem; border-top: 3px solid #2a9d4a; }
.result-label { font-weight: 700; margin-bottom: .4rem; color: #1a6630; }
.result-items { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .5rem; }
.result-item { padding: .3rem .65rem; border-radius: 20px; font-size: .88rem; font-weight: 600; color: #fff; }
.both { background: #2a7d4a; }
.alice-only { background: #1d3557; }
.bob-only { background: #457b9d; }
.privacy-note { font-size: .8rem; color: #444; line-height: 1.45; }
.legend { display: flex; gap: .8rem; flex-wrap: wrap; font-size: .78rem; margin-top: .5rem; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 3px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Shared universe of possible items (both parties know this label space).
const UNIVERSE = ['A','B','C','D','E','F','G','H'];
// Default private sets: Alice={A,B,C,D}, Bob={C,D,E,F}.
const aliceSet = new Set(['A','B','C','D']);
const bobSet = new Set(['C','D','E','F']);
function renderParty(containerId, universe, activeSet, cssOff, cssOn) {
// Render one party’s item toggles.
const el = document.getElementById(containerId);
el.innerHTML = '';
for (const item of universe) {
const d = document.createElement('span');
d.className = 'item ' + (activeSet.has(item) ? cssOn : cssOff);
d.textContent = item;
d.addEventListener('click', () => {
activeSet.has(item) ? activeSet.delete(item) : activeSet.add(item);
renderParty(containerId, universe, activeSet, cssOff, cssOn);
document.getElementById('result-area').style.display = 'none';
document.getElementById('status').textContent = '';
});
el.appendChild(d);
}
}
function runProtocol() {
// Simulate the PSU protocol: compute union, reveal it, but annotate what was private.
if (aliceSet.size === 0 && bobSet.size === 0) {
const s = document.getElementById('status');
s.textContent = 'Select at least one item in a set before running.'; s.className = 'status err'; return;
}
document.getElementById('status').textContent = '';
const union = new Set([...aliceSet, ...bobSet]);
const ri = document.getElementById('result-items');
ri.innerHTML = '';
for (const item of UNIVERSE) {
if (!union.has(item)) continue;
const d = document.createElement('span');
const inA = aliceSet.has(item), inB = bobSet.has(item);
d.className = 'result-item ' + (inA && inB ? 'both' : inA ? 'alice-only' : 'bob-only');
d.textContent = item;
ri.appendChild(d);
}
// Count items each party held exclusively (the other party did not know these before).
const alicePrivate = [...aliceSet].filter(x => !bobSet.has(x)).length;
const bobPrivate = [...bobSet].filter(x => !aliceSet.has(x)).length;
document.getElementById('privacy-note').innerHTML =
i18nFmt('Alice has %ap% item(s) not in Bob’s set; Bob has %bp% item(s) not in Alice’s set. Each party learns only which of its private items appear in the union — nothing else about the other’s set.', { ap: alicePrivate, bp: bobPrivate }) +
'<div class="legend">' +
'<span><span class="legend-dot" style="background:#2a7d4a"></span>In both sets</span>' +
'<span><span class="legend-dot" style="background:#1d3557"></span>Alice only</span>' +
'<span><span class="legend-dot" style="background:#457b9d"></span>Bob only</span>' +
'</div>';
document.getElementById('result-area').style.display = 'block';
}
function resetAll() {
// Restore default sets and hide the result panel.
aliceSet.clear(); ['A','B','C','D'].forEach(x => aliceSet.add(x));
bobSet.clear(); ['C','D','E','F'].forEach(x => bobSet.add(x));
renderParty('alice-items', UNIVERSE, aliceSet, 'alice-off', 'alice-on');
renderParty('bob-items', UNIVERSE, bobSet, 'bob-off', 'bob-on');
document.getElementById('result-area').style.display = 'none';
document.getElementById('status').textContent = '';
}
document.getElementById('run').addEventListener('click', runProtocol);
document.getElementById('reset').addEventListener('click', resetAll);
renderParty('alice-items', UNIVERSE, aliceSet, 'alice-off', 'alice-on');
renderParty('bob-items', UNIVERSE, bobSet, 'bob-off', 'bob-on');
<!-- Private Set Union demo: two parties compute A ∪ B without revealing their private elements. -->
<div class="parties">
<div class="party" id="alice-panel">
<div class="party-label">Alice’s private set</div>
<div class="party-hint">Click items to toggle. These are Alice’s private elements.</div>
<div class="items" id="alice-items"></div>
</div>
<div class="party" id="bob-panel">
<div class="party-label">Bob’s private set</div>
<div class="party-hint">Click items to toggle. These are Bob’s private elements.</div>
<div class="items" id="bob-items"></div>
</div>
</div>
<div class="btns">
<button id="run" type="button">Run protocol</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div class="result-area" id="result-area" style="display:none">
<div class="result-label">Union result (A ∪ B)</div>
<div class="result-items" id="result-items"></div>
<div class="privacy-note" id="privacy-note"></div>
</div>
/* Layout and color scheme for the PSU demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.parties { display: flex; gap: 1rem; margin-bottom: .8rem; flex-wrap: wrap; }
.party { flex: 1 1 200px; background: #f0f4f8; border-radius: 10px; padding: .8rem 1rem; }
#alice-panel { border-top: 3px solid #1d3557; }
#bob-panel { border-top: 3px solid #457b9d; }
.party-label { font-weight: 700; font-size: 1rem; margin-bottom: .25rem; }
.party-hint { font-size: .82rem; color: #555; margin-bottom: .5rem; }
.items { display: flex; flex-wrap: wrap; gap: .4rem; }
.item { padding: .3rem .65rem; border-radius: 20px; font-size: .88rem; font-weight: 600;
cursor: pointer; border: 2px solid transparent; transition: all .12s; user-select: none; }
.item.alice-off { background: #dce8f5; color: #1d3557; border-color: #aac4e0; }
.item.alice-on { background: #1d3557; color: #fff; border-color: #1d3557; }
.item.bob-off { background: #d6eaf6; color: #457b9d; border-color: #90c0da; }
.item.bob-on { background: #457b9d; color: #fff; border-color: #457b9d; }
.item:hover { opacity: .82; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.err { color: #c92f3c; }
.result-area { background: #eafaea; border-radius: 10px; padding: .8rem 1rem; border-top: 3px solid #2a9d4a; }
.result-label { font-weight: 700; margin-bottom: .4rem; color: #1a6630; }
.result-items { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .5rem; }
.result-item { padding: .3rem .65rem; border-radius: 20px; font-size: .88rem; font-weight: 600; color: #fff; }
.both { background: #2a7d4a; }
.alice-only { background: #1d3557; }
.bob-only { background: #457b9d; }
.privacy-note { font-size: .8rem; color: #444; line-height: 1.45; }
.legend { display: flex; gap: .8rem; flex-wrap: wrap; font-size: .78rem; margin-top: .5rem; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 3px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Shared universe of possible items (both parties know this label space).
const UNIVERSE = ['A','B','C','D','E','F','G','H'];
// Default private sets: Alice={A,B,C,D}, Bob={C,D,E,F}.
const aliceSet = new Set(['A','B','C','D']);
const bobSet = new Set(['C','D','E','F']);
function renderParty(containerId, universe, activeSet, cssOff, cssOn) {
// Render one party’s item toggles.
const el = document.getElementById(containerId);
el.innerHTML = '';
for (const item of universe) {
const d = document.createElement('span');
d.className = 'item ' + (activeSet.has(item) ? cssOn : cssOff);
d.textContent = item;
d.addEventListener('click', () => {
activeSet.has(item) ? activeSet.delete(item) : activeSet.add(item);
renderParty(containerId, universe, activeSet, cssOff, cssOn);
document.getElementById('result-area').style.display = 'none';
document.getElementById('status').textContent = '';
});
el.appendChild(d);
}
}
function runProtocol() {
// Simulate the PSU protocol: compute union, reveal it, but annotate what was private.
if (aliceSet.size === 0 && bobSet.size === 0) {
const s = document.getElementById('status');
s.textContent = 'Select at least one item in a set before running.'; s.className = 'status err'; return;
}
document.getElementById('status').textContent = '';
const union = new Set([...aliceSet, ...bobSet]);
const ri = document.getElementById('result-items');
ri.innerHTML = '';
for (const item of UNIVERSE) {
if (!union.has(item)) continue;
const d = document.createElement('span');
const inA = aliceSet.has(item), inB = bobSet.has(item);
d.className = 'result-item ' + (inA && inB ? 'both' : inA ? 'alice-only' : 'bob-only');
d.textContent = item;
ri.appendChild(d);
}
// Count items each party held exclusively (the other party did not know these before).
const alicePrivate = [...aliceSet].filter(x => !bobSet.has(x)).length;
const bobPrivate = [...bobSet].filter(x => !aliceSet.has(x)).length;
document.getElementById('privacy-note').innerHTML =
i18nFmt('Alice has %ap% item(s) not in Bob’s set; Bob has %bp% item(s) not in Alice’s set. Each party learns only which of its private items appear in the union — nothing else about the other’s set.', { ap: alicePrivate, bp: bobPrivate }) +
'<div class="legend">' +
'<span><span class="legend-dot" style="background:#2a7d4a"></span>In both sets</span>' +
'<span><span class="legend-dot" style="background:#1d3557"></span>Alice only</span>' +
'<span><span class="legend-dot" style="background:#457b9d"></span>Bob only</span>' +
'</div>';
document.getElementById('result-area').style.display = 'block';
}
function resetAll() {
// Restore default sets and hide the result panel.
aliceSet.clear(); ['A','B','C','D'].forEach(x => aliceSet.add(x));
bobSet.clear(); ['C','D','E','F'].forEach(x => bobSet.add(x));
renderParty('alice-items', UNIVERSE, aliceSet, 'alice-off', 'alice-on');
renderParty('bob-items', UNIVERSE, bobSet, 'bob-off', 'bob-on');
document.getElementById('result-area').style.display = 'none';
document.getElementById('status').textContent = '';
}
document.getElementById('run').addEventListener('click', runProtocol);
document.getElementById('reset').addEventListener('click', resetAll);
renderParty('alice-items', UNIVERSE, aliceSet, 'alice-off', 'alice-on');
renderParty('bob-items', UNIVERSE, bobSet, 'bob-off', 'bob-on');
Comments
Loading comments...