// SPDX-License-Identifier: Apache-2.0 // © 2026 Lutar, Stephen P. — SZL Holdings // ORCID: 0009-0001-0110-4173 // // Layer 6 — a11oy policy gate for RDPSequentialComposition (G38) // // Policy rationale: // For a pipeline of k steps each satisfying (α, εᵢ)-RDP, the sequential // composition is (α, Σεᵢ)-RDP (Mironov 2017, Proposition 1). // Converting to (ε_dp, δ)-DP: ε_dp = Σεᵢ + ln(1/δ)/(α-1). // The gate accepts a chained receipt only if ε_dp ≤ declared budget ceiling. // // Lean theorem cited: `rdpSequentialCompositionAdditivity` // Lean file: Lutar/DP/RDPComposition.lean // Lean commit SHA: b675cd84caa17080671570c153484c817f8769ac // Lean status: theorem (2 sorries for measure-theoretic Rényi divergence; // budget arithmetic predicate `rdpBudgetValid` has 0 sorries) // Severity: ENFORCED // // References: // Mironov (2017). "Rényi Differential Privacy." // IEEE CSF 2017. arXiv:1702.07476. DOI:10.1109/CSF.2017.11 // Rényi (1961). Proc. 4th Berkeley Symposium 1, 547–561. // https://projecteuclid.org/euclid.bsmsp/1200512181 // ── Inline formula ───────────────────────────────────────────────────────────── // G38: ε_total = Σᵢ εᵢ (composition, same α) // ε_dp = ε_total + ln(1/δ) / (α - 1) (conversion to (ε,δ)-DP) // Gate passes iff ε_dp ≤ budget_threshold. export interface RDPCompositionGateConfig { /** * Default Rényi order α if not declared per-receipt. * Must be > 1. Default: 8.0 (common choice for Gaussian mechanism). */ defaultAlpha?: number; /** * Default δ for RDP → DP conversion if not declared. * Default: 1e-5. */ defaultDelta?: number; } export interface RDPCompositionGateOpts { /** Rényi order α > 1. */ rdp_alpha: number; /** Per-step RDP ε values (one per sequential mechanism call). */ rdp_epsilon_steps: number[]; /** δ for the (ε, δ)-DP conversion. */ dp_delta: number; /** Declared ε budget ceiling. */ budget_epsilon_threshold: number; } export interface RDPCompositionDecision { allow: boolean; rationale: string; formula: string; leanTheorem: string; leanFile: string; leanCommitSha: string; rdp_alpha: number; rdp_epsilon_total: number; dp_epsilon_converted: number; dp_delta: number; budget_epsilon_threshold: number; rdp_budget_valid: boolean; step_count: number; dsse_extension: { rdp_accounting: { alpha: number; steps: number[]; epsilon_total_rdp: number; delta: number; epsilon_dp_converted: number; budget_threshold: number; budget_valid: boolean; lean_theorem_sha: string; }; }; } const LEAN_THEOREM = "rdpSequentialCompositionAdditivity"; const LEAN_FILE = "Lutar/DP/RDPComposition.lean"; const LEAN_COMMIT = "b675cd84caa17080671570c153484c817f8769ac"; const FORMULA_STR = "ε_dp = Σᵢεᵢ + ln(1/δ)/(α−1) [Mironov 2017 Prop.1+3; arXiv:1702.07476; DOI:10.1109/CSF.2017.11]"; /** * Compute total (α, Σεᵢ)-RDP cost from steps, then convert to (ε_dp, δ)-DP. * Mironov 2017 Proposition 1 (composition) and Proposition 3 (conversion). */ function computeRDPBudget( alpha: number, epsilonSteps: number[], delta: number ): { epsilon_total_rdp: number; epsilon_dp_converted: number } { // Composition: (α, ε₁)-RDP + (α, ε₂)-RDP = (α, ε₁+ε₂)-RDP const epsilon_total_rdp = epsilonSteps.reduce((s, e) => s + e, 0); // Conversion: (α, ε_rdp)-RDP → (ε_rdp + ln(1/δ)/(α-1), δ)-DP const epsilon_dp_converted = epsilon_total_rdp + Math.log(1 / delta) / (alpha - 1); return { epsilon_total_rdp, epsilon_dp_converted }; } /** * RDPSequentialComposition (G38) policy gate. * * Verifies that the cumulative RDP privacy budget across k sequential * mechanism calls, converted to (ε, δ)-DP, does not exceed the declared ceiling. * * Lean theorem: `rdpSequentialCompositionAdditivity` (Lutar/DP/RDPComposition.lean) * Reference: Mironov (2017) arXiv:1702.07476. */ export function rdpCompositionGate( config: RDPCompositionGateConfig = {} ): (opts: RDPCompositionGateOpts) => RDPCompositionDecision { const defaultAlpha = config.defaultAlpha ?? 8.0; const defaultDelta = config.defaultDelta ?? 1e-5; return (opts: RDPCompositionGateOpts): RDPCompositionDecision => { const { rdp_alpha, rdp_epsilon_steps, dp_delta, budget_epsilon_threshold, } = opts; // --- Input validation --- if (!Number.isFinite(rdp_alpha) || rdp_alpha <= 1) { throw new Error(`RDPCompositionGate: rdp_alpha must be > 1; got ${rdp_alpha}`); } if (!Array.isArray(rdp_epsilon_steps) || rdp_epsilon_steps.length === 0) { throw new Error("RDPCompositionGate: rdp_epsilon_steps must be a non-empty array"); } for (const e of rdp_epsilon_steps) { if (!Number.isFinite(e) || e < 0) { throw new Error(`RDPCompositionGate: all RDP ε steps must be ≥ 0; got ${e}`); } } if (!Number.isFinite(dp_delta) || dp_delta <= 0 || dp_delta >= 1) { throw new Error(`RDPCompositionGate: dp_delta must be in (0,1); got ${dp_delta}`); } // --- Core accounting --- const { epsilon_total_rdp, epsilon_dp_converted } = computeRDPBudget( rdp_alpha, rdp_epsilon_steps, dp_delta ); const rdp_budget_valid = epsilon_dp_converted <= budget_epsilon_threshold; const rationale = rdp_budget_valid ? `Lean:${LEAN_THEOREM} — RDP budget valid. ε_rdp=${epsilon_total_rdp.toFixed(6)}, ε_dp=${epsilon_dp_converted.toFixed(6)} ≤ budget=${budget_epsilon_threshold}. α=${rdp_alpha}, k=${rdp_epsilon_steps.length} steps.` : `Lean:${LEAN_THEOREM} — DENY: ε_dp=${epsilon_dp_converted.toFixed(6)} exceeds budget=${budget_epsilon_threshold}. (α=${rdp_alpha}, Σεᵢ=${epsilon_total_rdp.toFixed(6)}, δ=${dp_delta})`; return { allow: rdp_budget_valid, rationale, formula: FORMULA_STR, leanTheorem: LEAN_THEOREM, leanFile: LEAN_FILE, leanCommitSha: LEAN_COMMIT, rdp_alpha, rdp_epsilon_total: epsilon_total_rdp, dp_epsilon_converted: epsilon_dp_converted, dp_delta, budget_epsilon_threshold, rdp_budget_valid, step_count: rdp_epsilon_steps.length, dsse_extension: { rdp_accounting: { alpha: rdp_alpha, steps: rdp_epsilon_steps, epsilon_total_rdp, delta: dp_delta, epsilon_dp_converted, budget_threshold: budget_epsilon_threshold, budget_valid: rdp_budget_valid, lean_theorem_sha: LEAN_COMMIT, }, }, }; }; }