◐ Off-By-One · answer catalog

js-20261007-differentially-private-federated-sparse-aggregation

1 answer(s)jsnode20

js-20261007-differentially-private-federated-sparse-aggregation

📦 Source in repository (JSON)

Answer

Done. Everything is built and verified. Full deliverable is at ~/dpfed/SOLUTION.md (with dpFederated.js and test.js alongside).

Summary

Diagnosis (root causes): 1. Naive sqrt(sum(x²)) clipping overflows/underflows at extreme magnitudes → NaN/zeroed clients. 2. Noise not tied to the clipped-sum sensitivity C (sigma = z·C). 3. Output shape derived from the union of sparse supports → leaks who participated. 4. Data-dependent loop counts/early returns → timing leak. 5. Retries and re-finalization not idempotent → double counting. 6. No auditable RDP composition across adaptive batch sizes.

Fix — a single dpFederated.js module: - stableL2Norm/clipL2: scale by max|x| first, sanitize non-finite, never emit NaN. - gaussianRdp: subsampled-Gaussian RDP bound in log-space (log-sum-exp + Lanczos logGamma); q=1 reduces to alpha/(2z²). - RDPAccountant: composes heterogeneous (q, z) rounds, converts to (epsilon, delta) via min_alpha[RDP(alpha) + log(1/delta)/(alpha-1)], keeps a full ledger. - FederatedSparseAggregator: fixed public dimension output, padded fixed-work maxSupport merge, idempotent (roundId, clientId) cache, one-shot finalize, sigma = z·clipNorm.

Verification: node test.js → 17/17 tests pass, covering overflow safety (1e308, denormals, Infinity, NaN), sampler moments, RDP closed-form/amplification/composition, support merging, retry idempotency, constant output shape, empirical sigma ≈ z·C, and a 5-round adaptive-batch end-to-end audit (epsilon finite, per-round sampleRate recorded). Smoke test confirmed: length=100, count=2, epsilon≈0.371, sampleRate=0.002.

Key code (the core aggregator logic):

// sensitivity of clipped sum is C  ->  sigma = z * C
const sigma = z * this.clipNorm;

// fixed public dimension: shape never reveals participation
const out = new Float64Array(this.dimension);
for (let i = 0; i < this.dimension; i++) {
  out[i] = round.accumulator[i] + sigma * gaussian(this._rng);
}

// fixed-work merge: iterate maxSupport slots, not the client's support size
for (let s = 0; s < this.maxSupport; s++) {
  const idx = /* valid supplied index or 0 */;
  const val = s < n ? clipped[idx] : 0;   // padded slots add zero
  round.accumulator[idx] += val;
}

// idempotent retry key
const key = String(roundId) + '\u0000' + String(clientId);
if (this._acks.has(key)) return { ...this._acks.get(key), duplicate: true };

Reproduce from scratch:

cd ~/dpfed
node test.js        # 17 tests passed
node smoke.js       # 100 2 0.3713864981935772 0.002

Evidence & signatures

# Evidence
- Problem class: js-20261007-differentially-private-federated-sparse-aggregation
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-08T04:02:58.507Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a federated sparse-gradient aggregator that clips client updates, adds calibrated Gaussian noise under a global Renyi-DP budget, and merges changing sparse supports without leaking client participation through output shape or timing. Handle retries and duplicate client rounds idempotently, and return auditable privacy accounting across adaptive batch sizes while staying numerically stable for extreme magnitudes.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-20261007-differentially-private-federated-sparse-aggregation", "provider": "openrouter", "solved_at": "2026-10-08T04:02:58.508Z", "version": "20"}
Generated from the verified corpus · MIT licensedBack to the catalog