diff --git a/LatticeCrypto/MLDSA/SecurityNMA.lean b/LatticeCrypto/MLDSA/SecurityNMA.lean index 00800ed26..2544489db 100644 --- a/LatticeCrypto/MLDSA/SecurityNMA.lean +++ b/LatticeCrypto/MLDSA/SecurityNMA.lean @@ -6,6 +6,8 @@ Authors: Oleksandr Vovkotrub module public import LatticeCrypto.MLDSA.Security +public import VCVio.CryptoFoundations.Asymptotics.Negligible +public import Mathlib.Analysis.SpecificLimits.Normed /-! # ML-DSA EUF-NMA Security: reduction scaffolding @@ -22,10 +24,11 @@ This file builds the reduction infrastructure for the ML-DSA EUF-NMA analysis: key-generator prefix out of the NMA runtime. The older full-ring `mldsaMLWE` definitions remain useful scaffolding, but do not identify `keygen0` with a literature MLWE distribution. 3. **SelfTargetMSIS extraction (`nmaAdvantage_keygen1_le_stmsis`).** Once `t` is uniform the key - carries no secret, so a forgery is a short vector satisfying the SelfTargetMSIS relation; the - extractor `extractorC` reads `(z, c̃)` out of the forged signature. This is fully proven: the + carries no secret, so a forgery is a short vector satisfying the *tailored* SelfTargetMSIS + relation of `mldsaSTMSIS` (see *Tailored vs. standard SelfTargetMSIS* below); the extractor + `extractorC` reads `(z, c̃)` out of the forged signature. This is fully proven: the shared random-oracle simulation lines up the NMA `verify` query with the extractor's RO read-back - (`stmsis_tail_le`), and an accepted forgery is a valid SelfTargetMSIS solution by commitment + (`stmsis_tail_le`), and an accepted forgery is a valid solution of that problem by commitment recoverability. The `H₁` reprogramming step of the paper folds into the random-oracle modeling and is not separated @@ -51,6 +54,23 @@ the matrix is *defined* as ` := ExpandA(ρ)` wherever it is used, so that This is the standard ROM modeling of Dilithium with `ExpandA` a random oracle, and it makes the distinguisher `B` total: it consumes `(ρ, t)` and forms `pk = (ρ, Power2Round(t).1)` directly with no embedding witness required. + +## Tailored vs. standard SelfTargetMSIS + +The problems `mldsaSTMSIS` and `mldsaSTMSISShort` are **tailored** SelfTargetMSIS problems: their +validity predicate *is* the ML-DSA verifier relation, namely the norm gates `‖z‖∞ < γ₁ − β` and +`weight(h) ≤ ω`, the hint-recovered equation +`w' = UseHint(h, ·z − SampleInBall(c̃)·(t₁·2^d))` over `R_q`, and the self-target binding +`hashInput.2 = w'` (with the RO consistency of `c̃` supplied by the surrounding +`SelfTargetMSIS.experiment`). What is proved here is the extraction into that tailored problem +together with its algebraic characterization (`stmsisAlgebraicSolution`, +`mldsaSTMSISShort_isValid_iff`, `mldsaSTMSISShort_isValid_expandA_iff`). + +This is deliberately *not* the standard SelfTargetMSIS normal form used in the literature, which +states the linear relation as `[I_m | A] · y` with the challenge occupying the final coefficient +block of the short preimage `y`. Reducing the tailored relation to that normal form — absorbing +`UseHint` and the `2^d` shift into a single short vector — is follow-up work, and no declaration +in this file claims it. -/ @[expose] public section @@ -230,6 +250,64 @@ theorem keygenShort_generable : hr.gen = keygenShort p prims := ⟨hrShort p prims, rfl⟩ +/-- The generable relation carried by the FIPS seed-derived key generation: the generator is +`keygen0`, and every generated pair is seed-valid. Each pair drawn by `keygen0` is literally +the key assembled by `keyFromMaterial` from the material expanded out of its seed, which +`keyFromMaterial_eq` identifies with `keyGenFromSeed` — exactly the witness `validKeyPair` +asks for. This inhabits the `hGen` hypothesis of the FIPS-keygen security corollary +(`keygen0_generable`). -/ +def hrFips : + GenerableRelation (PublicKey p prims) (SecretKey p) (validKeyPair p prims) := + ⟨keygen0 p prims, fun pk sk hmem => by + rw [validKeyPair_eq_true_iff] + simp only [keygen0, mem_support_bind_iff] at hmem + obtain ⟨seed, -, hpure⟩ := hmem + refine ⟨seed, ?_⟩ + have h := (eq_of_mem_support_pure _ hpure).symm + simpa only [keyFromMaterial, keyGenFromSeed] using h⟩ + +omit [DecidableEq prims.High] [SampleableType (RqVec p.l)] [SampleableType (RqVec p.k)] in +/-- **Satisfiability certificate for the FIPS-keygen `hGen` hypothesis.** Some generable +relation over `validKeyPair` has the seed-derived FIPS key generator `keygen0` as its +generator — witnessed by `hrFips`. The FIPS-keygen security corollary hypothesizes such a +relation via `hGen : hr.gen = keygen0 p prims`; this theorem records that the hypothesis +pair `(hr, hGen)` is inhabited, so that statement has non-vacuous instances. -/ +theorem keygen0_generable : + ∃ hr : GenerableRelation (PublicKey p prims) (SecretKey p) (validKeyPair p prims), + hr.gen = keygen0 p prims := + ⟨hrFips p prims, rfl⟩ + +/-- **XOF replacement for the ML-DSA secret derivation (`ExpandSeed`/`ExpandS`), quantified +form.** For a real bound `εPRG`, this asserts that no distinguisher receiving +`(ρ, K, s₁, s₂)` can tell the FIPS derivation — expand a uniform 32-byte seed into +`(ρ, ρ', K)` and derive `(s₁, s₂) = ExpandS(ρ')` — from independent sampling with the +correct short marginals: `ρ`, `K` uniform and `(s₁, s₂)` uniform on the `η`-bounded box +`S_η^ℓ × S_η^k`, i.e. exactly the draws of the idealized key generator `keygenShort`. + +This is the standard PRG/XOF-replacement reading of `ExpandSeed`/`ExpandS` against the +short-secret marginal: the ideal branch is the box distribution the Module-LWE assumption +for ML-DSA is stated over, so the assumption carries exactly the "SHAKE output is +pseudorandom with the FIPS marginals" step and nothing else. For a fixed deterministic +`prims` the unrestricted-quantifier form is only satisfiable at large `εPRG` — an unbounded +distinguisher can test membership in the `2^256`-point image of the seed expansion — so, +pending the cost-model infrastructure (#460), it should be read computationally, against +bounded distinguishers, where it is the assumption that the SHAKE-derived `(ρ, K, s₁, s₂)` +is pseudorandom with the FIPS marginals. It is consumed by the FIPS-keygen corollary +`nma_security_fips` to transfer the short-model bound to `keygen0`. -/ +def expandSReplacement (εPRG : ℝ) : Prop := + ∀ D : Bytes 32 → Bytes 32 → RqVec p.l → RqVec p.k → ProbComp Bool, + |(Pr[= true | do + let seed ← $ᵗ (Bytes 32) + let (rho, rhoPrime, key) := prims.expandSeed seed + let (s1, s2) := prims.expandS rhoPrime + D rho key s1 s2]).toReal - + (Pr[= true | do + let key ← $ᵗ (Bytes 32) + let rho ← $ᵗ (Bytes 32) + let s1 ← sampleShortVec p.l p.eta + let s2 ← sampleShortVec p.k p.eta + D rho key s1 s2]).toReal| ≤ εPRG + end KeyGen section Game @@ -972,10 +1050,1010 @@ theorem nmaAdvantage_keygen1_le_stmsis dsimp only cases cache hashInput <;> rfl +/-! ### Tailored SelfTargetMSIS leg in the idealized short-key model + +The declarations below add the short-model counterpart of the SelfTargetMSIS extraction: +the tailored problem `mldsaSTMSISShort` (self-target binding + the short-scheme verifier), its +algebraic characterization lemmas, the extractor `extractorCShort`, and the NMA-to-STMSIS +extraction bound `nmaAdvantage_keygenShort1_le_stmsis`. They reuse the shared extractor +`extractorC` and the seed-based helpers from the enclosing section. -/ + +/-- **The SelfTargetMSIS problem embedded by ML-DSA verification in the idealized short-key +model.** The validity predicate recovers the +commitment `w'` from `(pk, c̃, (z, h))` via `UseHint ∘ computeWApprox`, requires it to equal +the commitment component of the hash preimage (the self-target binding), and runs the +identification-scheme verifier (the short-scheme constant `identificationSchemeShort`), +and the parameters are sampled from the idealized +uniform-`t` key generator `keygenShort1`: the matrix seed `ρ`, the signing key `K`, and the +short secrets are drawn independently, `t` is uniform, and the published pair is +`(ExpandA(ρ), pk)` with `pk = ⟨ρ, Power2Round(t).1⟩`. This is the STMSIS instance matching the +exact short-model key-swap hop (`nma_keyswap_hop_short`). + +Accepted solutions are characterized algebraically by `stmsisAlgebraicSolution` via the +bridge `mldsaSTMSISShort_isValid_iff`: the verifier's norm gates `‖z‖∞ < γ₁ − β` and +`weight(h) ≤ ω`, the hint-recovered matrix equation +`w' = UseHint(h, ·z − SampleInBall(c̃)·(t₁·2^d))` over `R_q`, and the self-target binding +`hashInput.2 = w'` tying the recovered commitment to the pair hashed to produce `c̃`, whose +RO consistency is enforced by the surrounding `SelfTargetMSIS.experiment`. At the matched +parameters published by `sampleParams` acceptance is the norm gates plus the binding +(`mldsaSTMSISShort_isValid_expandA_iff`). The relation is the tailored verifier relation, not +the standard SelfTargetMSIS normal form `[I_m | A] · y` with the challenge in the final +coefficient block of `y`; reducing the tailored relation to that normal form is follow-up +work. -/ +noncomputable def mldsaSTMSISShort (M : Type) : + SelfTargetMSIS.Problem (TqMatrix p.k p.l) (Response p prims) (PublicKey p prims) + (M × Commitment p prims) (CommitHashBytes p) where + sampleParams := do + let (pk, _) ← keygenShort1 p prims + return (prims.expandA pk.rho, pk) + isValid := fun aHat pk hashInput cTilde (z, h) => + -- Recover the commitment `w'` from `(pk, c̃, (z, h))`, bind it to the commitment component + -- of the hashed preimage, and run the identification verifier. + let w' := prims.useHintVec h (computeWApprox p prims aHat (prims.sampleInBall cTilde) z pk.t1) + decide (hashInput.2 = w') && (identificationSchemeShort p prims).verify pk w' cTilde (z, h) + +/-! ### Algebraic content of the tailored SelfTargetMSIS problem + +`mldsaSTMSISShort.isValid` is defined through the identification-scheme verifier plus the +self-target binding. The declarations below re-express an accepted solution in explicit +algebraic form — the norm gates, the hint-recovered matrix equation over `R_q`, and the +binding of the recovered commitment to the hashed preimage. That algebraic form is the +endpoint reached here: it is the tailored verifier relation, and reducing it to the standard +SelfTargetMSIS normal form `[I_m | A] · y` (challenge in the final coefficient block of the +short preimage `y`) is follow-up work. -/ + +omit [DecidableEq prims.High] [DecidableEq (Commitment p prims)] [SampleableType (RqVec p.l)] + [SampleableType (RqVec p.k)] [SampleableType (CommitHashBytes p)] in +/-- Under the transform laws, the verifier's recomputation `computeWApprox` is the plain +coefficient-domain matrix expression `·z − c·(t₁·2^d)`: the transform round trip +disappears, `*`/`•` are the transform-backed matrix-vector and scalar-vector products on +`R_q`, and `t₁·2^d = power2RoundShiftVec t₁`. Only the transform-isomorphism laws are +consumed (`unhatVec_sub`); both summands are definitionally the coefficient-domain +products. -/ +theorem computeWApprox_eq_mul_sub_smul (h_transform : NTTRingLaws nttOps) + (aHat : TqMatrix p.k p.l) (c : ChallengePoly) (z : RqVec p.l) + (t1 : Vector prims.Power2High p.k) : + computeWApprox p prims aHat c z t1 = + aHat * z - c • prims.power2RoundShiftVec t1 := by + haveI := h_transform + simp only [computeWApprox] + exact nttOps.unhatVec_sub _ _ + +omit [DecidableEq (Commitment p prims)] [SampleableType (RqVec p.k)] + [SampleableType (CommitHashBytes p)] in +/-- **What the identification verifier's accept means algebraically.** With +`c = SampleInBall(c̃)`, the verifier accepts `(w₁, c̃, (z, h))` exactly when the norm gates +`‖z‖∞ < γ₁ − β` and `weight(h) ≤ ω` hold and the published commitment `w₁` satisfies the +self-target matrix equation `UseHint(h, ExpandA(ρ)·z − c·(t₁·2^d)) = w₁` over `R_q`. In the +Fiat-Shamir game `w₁` is the very commitment hashed to produce `c̃`, so an accepted NMA +forgery carries the tailored algebraic verifier relation, which is exactly the relation the +tailored problem `mldsaSTMSISShort` checks. Reducing that relation to the standard +SelfTargetMSIS normal form `[I_m | A] · y`, with the challenge in the final coefficient block +of the short preimage `y`, remains follow-up work. + +Only the transform-isomorphism laws `NTTRingLaws` are consumed (via +`computeWApprox_eq_mul_sub_smul`), not the full `Primitives.Laws`. -/ +theorem identificationSchemeShort_verify_eq_true_iff (h_transform : NTTRingLaws nttOps) + (pk : PublicKey p prims) (w1 : Commitment p prims) (cTilde : CommitHashBytes p) + (z : RqVec p.l) (h : Vector prims.Hint p.k) : + (identificationSchemeShort p prims).verify pk w1 cTilde (z, h) = true ↔ + polyVecNorm z < p.gamma1 - p.beta ∧ + prims.hintWeight h ≤ p.omega ∧ + prims.useHintVec h (prims.expandA pk.rho * z - + prims.sampleInBall cTilde • prims.power2RoundShiftVec pk.t1) = w1 := by + simp only [identificationSchemeShort, identificationScheme, + computeWApprox_eq_mul_sub_smul p prims h_transform, Bool.and_eq_true, + decide_eq_true_eq] + tauto + +/-- **The explicit algebraic SelfTargetMSIS relation extracted from `mldsaSTMSISShort`.** +Writing `c = SampleInBall(c̃)` and `t₁·2^d = power2RoundShiftVec t₁`, a solution `(z, h)` +for an instance matrix `Â` and target `pk = (ρ, t₁)` consists of: + +1. the verifier's **norm gates**, verbatim: `‖z‖∞ < γ₁ − β` and `weight(h) ≤ ω`; +2. the **matrix equation**: a commitment `w'` recovered from the hint, + `w' = UseHint(h, ·z − c·(t₁·2^d))` over `R_q` (the coefficient-domain reading of + `computeWApprox`, see `computeWApprox_eq_mul_sub_smul`), which the verifier's own + recomputation from the published seed reproduces: + `UseHint(h, ExpandA(ρ)·z − c·(t₁·2^d)) = w'`; +3. the **self-target binding**: the commitment component of the hash preimage equals the + recovered commitment, `hashInput.2 = w'` — the solution is bound to the very pair hashed + to produce `c̃`. + +The **RO-consistency** of `c̃` is deliberately not part of the relation: it is enforced by +the surrounding `SelfTargetMSIS.experiment` (cache read-back), not by `isValid`. The +relation quantifies nothing `isValid` does not check — it is a re-expression of +`mldsaSTMSISShort.isValid` (`mldsaSTMSISShort_isValid_iff`), not a strengthening; on the +matched parameters ` = ExpandA(ρ)` published by `sampleParams` the two sides of the +recovered-commitment equation coincide and acceptance is the norm gates plus the binding +(`mldsaSTMSISShort_isValid_expandA_iff`). -/ +def stmsisAlgebraicSolution (aHat : TqMatrix p.k p.l) (pk : PublicKey p prims) + (hashInput : M × Commitment p prims) (cTilde : CommitHashBytes p) : + Response p prims → Prop + | (z, h) => + polyVecNorm z < p.gamma1 - p.beta ∧ + prims.hintWeight h ≤ p.omega ∧ + ∃ w' : Commitment p prims, + w' = prims.useHintVec h + (aHat * z - prims.sampleInBall cTilde • prims.power2RoundShiftVec pk.t1) ∧ + prims.useHintVec h (prims.expandA pk.rho * z - + prims.sampleInBall cTilde • prims.power2RoundShiftVec pk.t1) = w' ∧ + hashInput.2 = w' + +omit [DecidableEq M] [SampleableType (CommitHashBytes p)] in +/-- **The algebraic bridge for the tailored SelfTargetMSIS problem.** An accepted +`mldsaSTMSISShort` solution is exactly an `stmsisAlgebraicSolution`: the verifier's norm +gates, the hint-recovered matrix equation over `R_q` with the recovered commitment `w'` +exhibited explicitly, and the self-target binding of `w'` to the commitment component of +the hashed preimage. Only the transform-isomorphism laws `NTTRingLaws` are consumed (via +`computeWApprox_eq_mul_sub_smul`), not the full `Primitives.Laws`. The characterization is +of the tailored relation; the standard SelfTargetMSIS normal form `[I_m | A] · y` is not +reached here. -/ +theorem mldsaSTMSISShort_isValid_iff (h_transform : NTTRingLaws nttOps) + (aHat : TqMatrix p.k p.l) (pk : PublicKey p prims) (hashInput : M × Commitment p prims) + (cTilde : CommitHashBytes p) (z : RqVec p.l) (h : Vector prims.Hint p.k) : + (mldsaSTMSISShort p prims M).isValid aHat pk hashInput cTilde (z, h) = true ↔ + stmsisAlgebraicSolution p prims aHat pk hashInput cTilde (z, h) := by + simp only [mldsaSTMSISShort, identificationSchemeShort, identificationScheme, + stmsisAlgebraicSolution, computeWApprox_eq_mul_sub_smul p prims h_transform, + Bool.and_eq_true, decide_eq_true_eq] + constructor + · rintro ⟨hbind, ⟨hz, hw⟩, hweight⟩ + exact ⟨hz, hweight, _, rfl, hw, hbind⟩ + · rintro ⟨hz, hweight, w', rfl, hw, hbind⟩ + exact ⟨hbind, ⟨hz, hw⟩, hweight⟩ + +omit [DecidableEq M] [SampleableType (CommitHashBytes p)] in +/-- **Characterization at the matched parameters.** `mldsaSTMSISShort.sampleParams` always +publishes the matrix as ` = ExpandA(pk.ρ)`, and at such matched parameters the verifier's +own recomputation from the published seed coincides with the recovered commitment, so +acceptance is exactly the two norm gates plus the **self-target binding**: the commitment +component of the hashed preimage must equal the commitment +`UseHint(h, ·z − SampleInBall(c̃)·(t₁·2^d))` recomputed from the response (stated through +`computeWApprox`; see `computeWApprox_eq_mul_sub_smul` for the coefficient-domain reading). +In particular the trivial response `z = 0` with a weight-`0` hint wins only when the +adversary has hashed the exact commitment `UseHint(0, −SampleInBall(c̃)·(t₁·2^d))` — a value +determined by the challenge `c̃` that the random oracle returns only *after* the preimage is +fixed. No primitive laws are needed. -/ +theorem mldsaSTMSISShort_isValid_expandA_iff (pk : PublicKey p prims) + (hashInput : M × Commitment p prims) (cTilde : CommitHashBytes p) + (z : RqVec p.l) (h : Vector prims.Hint p.k) : + (mldsaSTMSISShort p prims M).isValid (prims.expandA pk.rho) pk hashInput cTilde + (z, h) = true ↔ + polyVecNorm z < p.gamma1 - p.beta ∧ prims.hintWeight h ≤ p.omega ∧ + hashInput.2 = prims.useHintVec h (computeWApprox p prims (prims.expandA pk.rho) + (prims.sampleInBall cTilde) z pk.t1) := by + simp only [mldsaSTMSISShort, identificationSchemeShort, identificationScheme, + Bool.and_eq_true, decide_eq_true_eq] + tauto + +/-- **The SelfTargetMSIS extractor for the idealized short-key model.** It performs the same +forger-to-preimage extraction as `extractorC` — run the NMA forger `main` on the target public +key, force the `H(msg, w')` query, and output the STMSIS preimage `(msg, w')` with the response +`(z, h)` — typed against the short-model problem `mldsaSTMSISShort`, whose parameters are sampled +from `keygenShort1`. -/ +noncomputable def extractorCShort [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))) : + SelfTargetMSIS.Adversary (mldsaSTMSISShort p prims M) := + ⟨(extractorC p prims main).run⟩ + +/-- **Per-key STMSIS read-back comparison, short model.** For a fixed public key `pk`, the +short-model NMA forge-and-verify tail (run +through `simulateToProbComp`) accepts no more often than the SelfTargetMSIS experiment tail of +`extractorCShort` at the matching parameters `(ExpandA(ρ), pk)`. The argument never inspects +the key relation: both tails simulate `main pk` against the same random oracle from the empty +cache, an aborting forgery contributes weight `0`, and on `some (w', (z, h))` both branches +issue the same `H(msg, w')` query, whose cached answer the STMSIS experiment reads back before +`mldsaSTMSISShort.isValid` recovers the commitment, binds it to the preimage component `w'`, +and runs the identical verifier. -/ +private theorem stmsis_tail_le_short + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) (validKeyPairShort p prims)) + (maxAttempts : ℕ) + (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))) + (pk : PublicKey p prims) : + Pr[= true | simulateToProbComp p prims (M := M) (do + let (msg, σ) ← main pk + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts).verify + pk msg σ)] ≤ + Pr[= true | do + let ((hashInput, response), cache) ← + (simulateQ (roImpl p prims (M := M)) + ((extractorCShort p prims main).run (prims.expandA pk.rho, pk))).run ∅ + match cache hashInput with + | some hashOutput => + pure ((mldsaSTMSISShort p prims M).isValid (prims.expandA pk.rho) pk + hashInput hashOutput response) + | none => pure false] := by + classical + -- Decompose both tails over the shared simulation of `main pk` from the empty cache. + unfold simulateToProbComp extractorCShort extractorC + simp only [bind_pure_comp, simulateQ_bind, StateT.run_bind, StateT.run'_eq, map_bind, + bind_assoc] + -- Compare after the shared `main pk` simulation prefix. + refine probOutput_bind_mono fun a _ => ?_ + -- `a = ((msg, σ), cache₀)`; split on whether the forgery aborts. + obtain ⟨⟨msg, σ⟩, cache0⟩ := a + cases σ with + | none => + -- Aborting forgery: NMA `verify` is deterministically `false`, so the NMA tail has weight `0`. + simp only [FiatShamirWithAbort, simulateQ_pure, StateT.run_pure, map_pure, + probOutput_pure] + simp + | some wzh => + obtain ⟨w', z, h⟩ := wzh + -- Non-aborting forgery `(w', (z, h))`. Both branches issue the same `H(msg, w')` query on + -- `cache0`; reduce the NMA `verify` and the extractor body to that single query. + simp only [FiatShamirWithAbort, simulateQ_map, StateT.run_map, bind_pure_comp] + -- Both sides are now `f <$> (simulateQ roImpl (query (msg, w'))).run cache0`; turn the maps + -- into binds over the shared random-oracle run and compare per random answer `(c, cache₁)`. + simp only [map_eq_bind_pure_comp, Function.comp_def, bind_assoc] + refine probOutput_bind_mono fun cc hcc => ?_ + simp only [pure_bind] + -- The query simulation caches its answer: `cc.2 (msg, w') = some cc.1`. + have hquery : simulateQ (roImpl p prims (M := M)) (query (msg, w') : + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) _) = + (randomOracle : QueryImpl (M × Commitment p prims →ₒ CommitHashBytes p) _) (msg, w') := + roSim.simulateQ_liftM_spec_query _ _ + rw [hquery] at hcc + have hcache : cc.2 (msg, w') = some cc.1 := by + cases hc0 : cache0 (msg, w') with + | some u => + rw [randomOracle, QueryImpl.withCaching_run_some _ hc0, support_pure, + Set.mem_singleton_iff] at hcc + subst hcc; exact hc0 + | none => + rw [randomOracle, QueryImpl.withCaching_run_none _ hc0, support_map] at hcc + obtain ⟨u, _, hu⟩ := hcc + subst hu + exact QueryCache.cacheQuery_self _ (msg, w') u + rw [hcache] + -- An accepted NMA forgery is a valid STMSIS solution: the middle conjunct of `verify` + -- says the recomputed commitment equals the forgery's `w'`, which is the commitment + -- component of the extractor's preimage `(msg, w')` — exactly the self-target binding + -- `isValid` demands. + rw [probOutput_pure, probOutput_pure] + by_cases hverify : + (identificationSchemeShort p prims).verify pk w' cc.1 (z, h) = true + · -- Accepted: `verify`'s middle conjunct identifies the recomputed commitment with `w'`, + -- so the binding conjunct holds at the preimage `(msg, w')` and `verify` re-accepts at + -- the recomputed commitment, giving `isValid = true`. + have hvalid : + (mldsaSTMSISShort p prims M).isValid (prims.expandA pk.rho) pk (msg, w') cc.1 + (z, h) = true := by + simp only [mldsaSTMSISShort, identificationSchemeShort, identificationScheme] + at hverify ⊢ + revert hverify + grind + rw [if_pos hverify.symm, if_pos hvalid.symm] + · simp only [Bool.not_eq_true] at hverify + rw [hverify] + simp + +/-- **The SelfTargetMSIS extraction bound in the idealized short-key model.** The uniform-`t` +short-model EUF-NMA advantage (key generator `keygenShort1`) is bounded by the SelfTargetMSIS +advantage of the extractor against `mldsaSTMSISShort`. + +The argument is a shared-prefix read-back comparison: after the +bundled-semantics rewrite (`nmaGameShort_eq_keygen_bind`) both sides bind over the same +`keygenShort1` prefix — the short problem's `sampleParams` is definitionally `keygenShort1` +followed by publishing `(ExpandA(ρ), pk)` — so monotonicity reduces to the per-key comparison +`stmsis_tail_le_short`, which never inspects the key distribution and packages the cache +read-back and commitment recoverability. -/ +theorem nmaAdvantage_keygenShort1_le_stmsis + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) (validKeyPairShort p prims)) + (maxAttempts : ℕ) + (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))) : + nmaAdvantageShort p prims hr maxAttempts (keygenShort1 p prims) main ≤ + SelfTargetMSIS.advantage (extractorCShort p prims main) := by + classical + rw [nmaAdvantageShort, nmaGameShort_eq_keygen_bind, SelfTargetMSIS.advantage, + SelfTargetMSIS.experiment] + rw [probOutput_def, SPMF.evalDist_def] + -- The short STMSIS `sampleParams` is exactly `keygenShort1` followed by publishing + -- `(ExpandA(ρ), pk)`, so both `Pr[= true]`s bind over the same prefix; compare them per-key. + change Pr[= true | (keygenShort1 p prims) >>= _] ≤ + Pr[= true | ((mldsaSTMSISShort p prims M).sampleParams) >>= _] + rw [show (mldsaSTMSISShort p prims M).sampleParams = + (keygenShort1 p prims) >>= fun pkSk => pure (prims.expandA pkSk.1.rho, pkSk.1) from rfl] + rw [bind_assoc] + refine probOutput_bind_mono ?_ + rintro ⟨pk, sk⟩ _ + rw [pure_bind] + convert stmsis_tail_le_short p prims hr maxAttempts main pk using 2 + rw [roImpl, unifFwdImpl] + refine bind_congr fun x => ?_ + obtain ⟨⟨hashInput, response⟩, cache⟩ := x + dsimp only + cases cache hashInput <;> rfl + end Extractor end NMA +open NMA + +section Headline + +variable (p : Params) (prims : Primitives p) [nttOps : NTTRingOps] + [DecidableEq prims.High] + {M : Type} [DecidableEq M] [DecidableEq (Commitment p prims)] + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + [SampleableType (RqVec p.l)] [SampleableType (RqVec p.k)] + [SampleableType (CommitHashBytes p)] + +open scoped Classical in +/-- **NMA security of ML-DSA in the idealized short-key model (Lemma 7, CRYPTO 2023).** + +For every EUF-NMA adversary `A` against the ML-DSA scheme (instantiated via `FiatShamirWithAbort` +over the idealized short-secret key generation `keygenShort`), there exist an MLWE adversary `B` +and a SelfTargetMSIS adversary `C` such that + + `Adv^{EUF-NMA}(A) ≤ Adv^{MLWE}(B) + Adv^{SelfTargetMSIS}(C)`. + +The reductions are the concrete ones built in this file: the key-swap distinguisher +`distinguisherBShort`, whose `mldsaMLWEShort` advantage **equals** the real-vs-uniform key gap — +the short key-swap hop `nma_keyswap_hop_short` is an exact monad identity, so no statistical +slack term appears in the bound — and the SelfTargetMSIS extractor `extractorCShort`, which +turns a uniform-`t` forgery into a short self-target solution +(`nmaAdvantage_keygenShort1_le_stmsis`). + +The hypothesis `hMlweBridge` supplies, for every forging strategy, an abstract MLWE adversary at +a bridge slack `εbridge`. Its canonical discharge lands on the uniform-matrix problem: take +`mlwe := mldsaMatrixMLWE p`, `εbridge := εA`, and for each `main` the witness +`matrixLift p prims (distinguisherBShort p prims hr maxAttempts main)` with the proven reduction +`advantage_mldsaMLWEShort_le_matrix` under the `expandAIdealization εA` assumption. + +Concretely, it supplies for every forging strategy an abstract MLWE adversary at +least as good (up to `εbridge`) as `distinguisherBShort` against the seed-based short problem +`mldsaMLWEShort` — the +distribution the ML-DSA Module-LWE assumption is stated over (secrets uniform on the `η`-bounded +box). Under `expandAIdealization` the bridge can be instantiated against the standard +uniform-matrix problem `mldsaMatrixMLWE` via `advantage_mldsaMLWEShort_le_matrix`. The +SelfTargetMSIS side has matching types, so `hStmsis` is a plain equality +`stmsis = mldsaSTMSISShort p prims M`, and `hGen : hr.gen = keygenShort p prims` pins the +Fiat-Shamir key generation to the idealized short-key generator. The relation of `hr` is the +material-based `validKeyPairShort`, which `keygenShort` genuinely generates: the pair +`(hr, hGen)` is inhabited by `hrShort` (`keygenShort_generable`), so the statement has +non-vacuous instances. + +This is the EUF-NMA half (Lemma 7) of the ML-DSA security proof in the idealized short-key model; +the CMA-to-NMA statistical step (`euf_cma_security_of_nma_short`) composes on top of it. -/ +theorem nma_security_short + (mlwe : LearningWithErrors.Problem (TqMatrix p.k p.l) (RqVec p.l) (RqVec p.k)) + (stmsis : SelfTargetMSIS.Problem + (TqMatrix p.k p.l) (Response p prims) + (PublicKey p prims) (M × Commitment p prims) (CommitHashBytes p)) + (maxAttempts : ℕ) + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) + (validKeyPairShort p prims)) + (hGen : hr.gen = keygenShort p prims) + (hStmsis : stmsis = mldsaSTMSISShort p prims M) + (εbridge : ℝ) + (hMlweBridge : ∀ (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))), + ∃ B : LearningWithErrors.Adversary mlwe, + LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims hr maxAttempts main) ≤ + LearningWithErrors.advantage mlwe B + εbridge) : + ∀ (adv : SignatureAlg.eufNmaAdv + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts)), + ∃ (mlweReduction : LearningWithErrors.Adversary mlwe) + (stmsisReduction : SelfTargetMSIS.Adversary stmsis), + adv.advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) ≤ + ENNReal.ofReal (LearningWithErrors.advantage mlwe mlweReduction + εbridge) + + SelfTargetMSIS.advantage stmsisReduction := by + classical + intro adv + obtain ⟨B, hB⟩ := hMlweBridge adv.main + subst hStmsis + refine ⟨B, extractorCShort p prims adv.main, ?_⟩ + -- The EUF-NMA experiment is the real-`t` short-model NMA game with `main := adv.main`. + have hadv : adv.advantage (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) = + nmaAdvantageShort p prims hr maxAttempts (keygenShort p prims) adv.main := by + rw [SignatureAlg.eufNmaAdv.advantage, nmaAdvantageShort, nmaGameShort] + rw [SignatureAlg.eufNmaExp] + simp only [FiatShamirWithAbort, hGen] + rfl + rw [hadv] + -- Bound the two NMA games by the MLWE distinguisher and the STMSIS extractor. + set pc0 := (do + let (pk, _) ← keygenShort p prims + simulateToProbComp p prims (M := M) (do + let (msg, σ) ← adv.main pk + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts).verify + pk msg σ) : ProbComp Bool) with hpc0 + set pc1 := (do + let (pk, _) ← keygenShort1 p prims + simulateToProbComp p prims (M := M) (do + let (msg, σ) ← adv.main pk + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts).verify + pk msg σ) : ProbComp Bool) with hpc1 + have hg0 : nmaAdvantageShort p prims hr maxAttempts (keygenShort p prims) adv.main = + Pr[= true | pc0] := by + rw [nmaAdvantageShort, nmaGameShort_eq_keygen_bind, probOutput_def, probOutput_def, + SPMF.evalDist_def] + have hg1 : nmaAdvantageShort p prims hr maxAttempts (keygenShort1 p prims) adv.main = + Pr[= true | pc1] := by + rw [nmaAdvantageShort, nmaGameShort_eq_keygen_bind, probOutput_def, probOutput_def, + SPMF.evalDist_def] + -- Triangle bound: real game ≤ uniform game + MLWE advantage. + have htri := ProbComp.probOutput_true_le_add_ofReal_boolDistAdvantage pc0 pc1 + rw [hg0] + refine le_trans htri ?_ + -- `pc0.boolDistAdvantage pc1 = |nmaAdv keygenShort - nmaAdv keygenShort1|`, which the exact + -- short key-swap hop bounds by the `mldsaMLWEShort` advantage — no statistical slack. + have hbias : pc0.boolDistAdvantage pc1 ≤ + LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims hr maxAttempts adv.main) := by + have hk := nma_keyswap_hop_short p prims hr maxAttempts (M := M) adv.main + rw [ProbComp.boolDistAdvantage, ← hg0, ← hg1] + exact hk + -- STMSIS extraction bound on the uniform game. + have hstm := nmaAdvantage_keygenShort1_le_stmsis p prims hr maxAttempts (M := M) adv.main + rw [hg1] at hstm + calc Pr[= true | pc1] + ENNReal.ofReal (pc0.boolDistAdvantage pc1) + ≤ SelfTargetMSIS.advantage (extractorCShort p prims adv.main) + + ENNReal.ofReal (LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims hr maxAttempts adv.main)) := + add_le_add hstm (ENNReal.ofReal_le_ofReal hbias) + _ ≤ SelfTargetMSIS.advantage (extractorCShort p prims adv.main) + + ENNReal.ofReal (LearningWithErrors.advantage mlwe B + εbridge) := + add_le_add le_rfl (ENNReal.ofReal_le_ofReal (le_trans hB le_rfl)) + _ = ENNReal.ofReal (LearningWithErrors.advantage mlwe B + εbridge) + + SelfTargetMSIS.advantage (extractorCShort p prims adv.main) := add_comm _ _ + +open scoped Classical in +/-- **NMA security of ML-DSA at the FIPS seed-derived key generation.** + +The short-model bound `nma_security_short` transferred to the deterministic FIPS key +generator `keygen0` through the XOF-replacement assumption `expandSReplacement`: for every +EUF-NMA adversary against the ML-DSA scheme instantiated with the seed-derived key relation +(`hGen : hr.gen = keygen0 p prims`, inhabited by `hrFips` / `keygen0_generable`), there are +an MLWE adversary and a SelfTargetMSIS adversary with + + `Adv^{EUF-NMA}(A) ≤ (Adv^{MLWE}(B) + εbridge) + Adv^{SelfTargetMSIS}(C) + εPRG`. + +The proof has exactly one new ingredient beyond the short model: the FIPS and short NMA +games share their forge-and-verify tail (`identificationScheme` and +`identificationSchemeShort` carry the same `verify` function), so the gap between the +`keygen0` game and the `keygenShort` game is one application of `hPRG` at the distinguisher +`D ρ K s₁ s₂ :=` "run the tail at the key built by `keyFromMaterial` from the material +`(ρ, K, s₁, s₂)`": its real branch is exactly the FIPS game and its ideal branch is exactly +the short game. The short-model reduction hypotheses (`hrS`/`hGenS`, `hStmsis`, +`hMlweBridge`) then bound the short game as in `nma_security_short`, applied to the same +forging strategy repackaged at the short scheme tag. -/ +theorem nma_security_fips + (mlwe : LearningWithErrors.Problem (TqMatrix p.k p.l) (RqVec p.l) (RqVec p.k)) + (stmsis : SelfTargetMSIS.Problem + (TqMatrix p.k p.l) (Response p prims) + (PublicKey p prims) (M × Commitment p prims) (CommitHashBytes p)) + (maxAttempts : ℕ) + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) + (validKeyPair p prims)) + (hGen : hr.gen = keygen0 p prims) + (hrS : GenerableRelation (PublicKey p prims) (SecretKey p) + (validKeyPairShort p prims)) + (hGenS : hrS.gen = keygenShort p prims) + (hStmsis : stmsis = mldsaSTMSISShort p prims M) + (εPRG : ℝ) (hPRG : expandSReplacement p prims εPRG) + (εbridge : ℝ) + (hMlweBridge : ∀ (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))), + ∃ B : LearningWithErrors.Adversary mlwe, + LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims hrS maxAttempts main) ≤ + LearningWithErrors.advantage mlwe B + εbridge) : + ∀ (adv : SignatureAlg.eufNmaAdv + (FiatShamirWithAbort (identificationScheme p prims) hr M maxAttempts)), + ∃ (mlweReduction : LearningWithErrors.Adversary mlwe) + (stmsisReduction : SelfTargetMSIS.Adversary stmsis), + adv.advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) ≤ + ENNReal.ofReal (LearningWithErrors.advantage mlwe mlweReduction + εbridge) + + SelfTargetMSIS.advantage stmsisReduction + + ENNReal.ofReal εPRG := by + classical + intro adv + obtain ⟨mlweRed, stmsisRed, hshortBound⟩ := + nma_security_short p prims mlwe stmsis maxAttempts hrS hGenS hStmsis εbridge hMlweBridge + ⟨adv.main⟩ + refine ⟨mlweRed, stmsisRed, ?_⟩ + -- The FIPS EUF-NMA experiment is the real-`t` NMA game at `keygen0` with `main := adv.main`. + have hadv : adv.advantage (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) = + nmaAdvantage p prims hr maxAttempts (keygen0 p prims) adv.main := by + rw [SignatureAlg.eufNmaAdv.advantage, nmaAdvantage, nmaGame] + rw [SignatureAlg.eufNmaExp] + simp only [FiatShamirWithAbort, hGen] + rfl + -- The two NMA games as plain `ProbComp`s over their key generators. + set pcF := (do + let (pk, _) ← keygen0 p prims + simulateToProbComp p prims (M := M) (do + let (msg, σ) ← adv.main pk + (FiatShamirWithAbort (identificationScheme p prims) hr M maxAttempts).verify + pk msg σ) : ProbComp Bool) with hpcF + set pcS := (do + let (pk, _) ← keygenShort p prims + simulateToProbComp p prims (M := M) (do + let (msg, σ) ← adv.main pk + (FiatShamirWithAbort (identificationSchemeShort p prims) hrS M maxAttempts).verify + pk msg σ) : ProbComp Bool) with hpcS + have hgF : nmaAdvantage p prims hr maxAttempts (keygen0 p prims) adv.main = + Pr[= true | pcF] := by + rw [nmaAdvantage, nmaGame_eq_keygen_bind, probOutput_def, probOutput_def, SPMF.evalDist_def] + have hgS : (⟨adv.main⟩ : SignatureAlg.eufNmaAdv + (FiatShamirWithAbort (identificationSchemeShort p prims) hrS M maxAttempts)).advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) = + Pr[= true | pcS] := by + have h1 : (⟨adv.main⟩ : SignatureAlg.eufNmaAdv + (FiatShamirWithAbort (identificationSchemeShort p prims) hrS M maxAttempts)).advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) = + nmaAdvantageShort p prims hrS maxAttempts (keygenShort p prims) adv.main := by + rw [SignatureAlg.eufNmaAdv.advantage, nmaAdvantageShort, nmaGameShort] + rw [SignatureAlg.eufNmaExp] + simp only [FiatShamirWithAbort, hGenS] + rfl + rw [h1, nmaAdvantageShort, nmaGameShort_eq_keygen_bind, probOutput_def, probOutput_def, + SPMF.evalDist_def] + -- The PRG hop: the two games are the two branches of `hPRG` at the shared verify tail. + have hF : Pr[= true | pcF] = Pr[= true | do + let seed ← $ᵗ (Bytes 32) + let (rho, rhoPrime, _key) := prims.expandSeed seed + let (s1, s2) := prims.expandS rhoPrime + simulateToProbComp p prims (M := M) (do + let d ← adv.main ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ + (FiatShamirWithAbort (identificationScheme p prims) hr M maxAttempts).verify + ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ d.1 d.2)] := by + rw [hpcF] + simp only [keygen0, keyFromMaterial, bind_assoc, pure_bind] + have hS : Pr[= true | pcS] = Pr[= true | do + let _key ← $ᵗ (Bytes 32) + let rho ← $ᵗ (Bytes 32) + let s1 ← sampleShortVec p.l p.eta + let s2 ← sampleShortVec p.k p.eta + simulateToProbComp p prims (M := M) (do + let d ← adv.main ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ + (FiatShamirWithAbort (identificationScheme p prims) hr M maxAttempts).verify + ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ d.1 d.2)] := by + rw [hpcS] + simp only [keygenShort, keyFromMaterial, bind_assoc, pure_bind] + rfl + have hbias : pcF.boolDistAdvantage pcS ≤ εPRG := by + rw [ProbComp.boolDistAdvantage, hF, hS] + exact hPRG (fun rho _key s1 s2 => simulateToProbComp p prims (M := M) (do + let d ← adv.main ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ + (FiatShamirWithAbort (identificationScheme p prims) hr M maxAttempts).verify + ⟨rho, (prims.power2RoundVec (prims.expandA rho * s1 + s2)).1⟩ d.1 d.2)) + -- Assemble: FIPS game ≤ short game + εPRG ≤ (MLWE + εbridge) + STMSIS + εPRG. + rw [hadv, hgF] + refine le_trans (ProbComp.probOutput_true_le_add_ofReal_boolDistAdvantage pcF pcS) ?_ + have hshort' : Pr[= true | pcS] ≤ + ENNReal.ofReal (LearningWithErrors.advantage mlwe mlweRed + εbridge) + + SelfTargetMSIS.advantage stmsisRed := by + rw [← hgS] + exact hshortBound + exact add_le_add hshort' (ENNReal.ofReal_le_ofReal hbias) + +open scoped Classical in +/-- **EUF-CMA security of the commitment-carrying short-key ML-DSA signature.** + +The CMA-to-NMA-to-hardness composition over the idealized short-secret key generation +`keygenShort`: for any EUF-CMA adversary `adv` against the Fiat-Shamir-with-aborts ML-DSA +signature, the advantage is bounded by the MLWE advantage, the SelfTargetMSIS advantage, and the +statistical CMA-to-NMA loss `FiatShamirWithAbort.cmaToNmaLoss`. The proof composes three pieces: + +1. `FiatShamirWithAbort.euf_cma_to_nma`: `adv.advantage ≤ Pr[managedRoNmaExp simulatedNmaAdv] + + cmaToNmaLoss`, under the good-key/commitment-guessing/abort/query hypotheses; +2. `FiatShamirWithAbort.managedRoNmaExp_simulatedNmaAdv_eq_eufNmaExp` (Option B): the managed-RO + NMA success probability equals the plain EUF-NMA advantage of `simulatedEufNmaAdv`, the + cache-forgetting reduction; +3. `nma_security_short` (Lemma 7, short model) applied to `simulatedEufNmaAdv`: + `≤ MLWE + SelfTargetMSIS`, with no statistical key-swap slack — the short-model hop is exact. + +The loss parameters carry the nonnegativity and good-key hypotheses that the abstract reduction +needs; the bridge hypotheses (`hGen`, `hStmsis`, `hMlweBridge`) pin the abstract hardness problems +to the concrete short-model ML-DSA ones (`keygenShort`, `mldsaSTMSISShort`, `mldsaMLWEShort`). +The relation of `hr` is the material-based `validKeyPairShort`, so `hGen` is inhabited by +`hrShort` (`keygenShort_generable`). The HVZK obligation `hhvzk` stays abstract: in the short +model the withheld key part `t₀` is not determined by the public key across material-valid +pairs, so no single simulator is exact-on-accept for every valid pair; the hypothesis is +satisfiable (any simulator at `ζ_zk = 1`, since `tvDist ≤ 1`), and a quantitative discharge +needs a bound accounting for the hint mismatch across colliding keys. + +**Scope: commitment-carrying, not yet the end-to-end FIPS CMA headline.** This theorem is +stated for the standard Fiat-Shamir-with-aborts signature whose commitment is *carried* in the +signature (the generic `Option (Commitment × Response)` type). FIPS-204 ML-DSA instead +*recovers* the commitment at verification rather than carrying it; reaching that form via +commitment recovery costs one extra hash query per signing query, so the end-to-end loss grows +by `qS` to `qH + qS` — the distinction recorded in `FiatShamirWithAbort.cmaToNmaLoss`. Supplying +that commitment-recovery bridge, and composing it with the FIPS seed-derived key generation, is +follow-up work: `nma_security_fips` currently gives the seed-derived-key result at the NMA level +only. -/ +theorem euf_cma_security_of_nma_short [SampleableType (PublicKey p prims)] + (mlwe : LearningWithErrors.Problem (TqMatrix p.k p.l) (RqVec p.l) (RqVec p.k)) + (stmsis : SelfTargetMSIS.Problem + (TqMatrix p.k p.l) (Response p prims) + (PublicKey p prims) (M × Commitment p prims) (CommitHashBytes p)) + (maxAttempts : ℕ) + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) + (validKeyPairShort p prims)) + (hGen : hr.gen = keygenShort p prims) + (hStmsis : stmsis = mldsaSTMSISShort p prims M) + (sim : PublicKey p prims → + ProbComp (Option (Commitment p prims × CommitHashBytes p × Response p prims))) + (ζ_zk : ℝ) (hζ : 0 ≤ ζ_zk) + (hhvzk : (identificationSchemeShort p prims).HVZK sim ζ_zk) + (qS qH : ℕ) (ε p_abort δ : ℝ) + (hε : 0 ≤ ε) (hδ : 0 ≤ δ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (Good : PublicKey p prims → SecretKey p → Prop) + (hGood : Pr[ fun xw : PublicKey p prims × SecretKey p => ¬ Good xw.1 xw.2 | hr.gen] ≤ + ENNReal.ofReal δ) + (hGuess : ∀ pk sk, Good pk sk → ∀ cm : Commitment p prims, + Pr[= cm | Prod.fst <$> (identificationSchemeShort p prims).commit pk sk] ≤ + ENNReal.ofReal ε) + (hAbort : ∀ pk sk, Good pk sk → + Pr[= none | (identificationSchemeShort p prims).honestExecution pk sk] ≤ + ENNReal.ofReal p_abort) + (hAbortSim : ∀ pk sk, Good pk sk → + Pr[= none | sim pk] ≤ ENNReal.ofReal p_abort) + (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts)) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commitment p prims × Response p prims)) (oa := adv.main pk) qS qH) + (εbridge : ℝ) + (hMlweBridge : ∀ (main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims))), + ∃ B : LearningWithErrors.Adversary mlwe, + LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims hr maxAttempts main) ≤ + LearningWithErrors.advantage mlwe B + εbridge) : + ∃ (mlweReduction : LearningWithErrors.Adversary mlwe) + (stmsisReduction : SelfTargetMSIS.Adversary stmsis), + adv.advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) ≤ + ENNReal.ofReal (LearningWithErrors.advantage mlwe mlweReduction + εbridge) + + SelfTargetMSIS.advantage stmsisReduction + + ENNReal.ofReal + (FiatShamirWithAbort.cmaToNmaLoss qS qH ε p_abort ζ_zk δ hp) := by + classical + -- Step 1: CMA advantage ≤ managed-RO NMA success of `simulatedNmaAdv` + loss. + have hcma := FiatShamirWithAbort.euf_cma_to_nma (identificationSchemeShort p prims) hr M + maxAttempts sim adv ζ_zk hζ hhvzk qS qH ε p_abort δ hε hδ hp₀ hp Good hGood hGuess + hAbort hAbortSim hQ + -- Step 2 (Option B bridge): managed-RO NMA success = plain EUF-NMA advantage of the + -- cache-forgetting reduction `simulatedEufNmaAdv`. + have hbridge := FiatShamirWithAbort.managedRoNmaExp_simulatedNmaAdv_eq_eufNmaExp + (identificationSchemeShort p prims) hr M maxAttempts sim adv + -- Step 3 (Lemma 7, short model): the plain EUF-NMA advantage is bounded by MLWE + STMSIS. + obtain ⟨mlweRed, stmsisRed, hnma⟩ := nma_security_short p prims mlwe stmsis maxAttempts + hr hGen hStmsis εbridge hMlweBridge + (FiatShamirWithAbort.simulatedEufNmaAdv (identificationSchemeShort p prims) hr M + maxAttempts sim adv) + refine ⟨mlweRed, stmsisRed, ?_⟩ + -- Assemble: advantage ≤ (managed = eufNma advantage ≤ MLWE + STMSIS) + loss. + refine le_trans hcma ?_ + have hmanaged : Pr[= true | SignatureAlg.managedRoNmaExp + (FiatShamirWithAbort.runtime M) + (FiatShamirWithAbort.simulatedNmaAdv (identificationSchemeShort p prims) hr M + maxAttempts sim adv)] = + (FiatShamirWithAbort.simulatedEufNmaAdv (identificationSchemeShort p prims) hr M + maxAttempts sim adv).advantage (FiatShamirWithAbort.runtime M) := by + rw [SignatureAlg.eufNmaAdv.advantage, hbridge] + rw [hmanaged] + exact add_le_add hnma le_rfl + +/-! ## Asymptotic (negligible) EUF-CMA headline + +The non-degenerate asymptotic statement. The scheme is indexed by a security parameter `n` +through a *family* `(p n, prims n)` of ML-DSA parameter/primitive instances, so that the +commitment guessing probability `ε n`, the key-regularity failure `δ n`, the HVZK slack +`ζ_zk n`, and the MLWE-bridge slack `εbridge n` all shrink (negligibly) as `n → ∞` while the +signing / hashing query budgets `qS n`, `qH n` grow only polynomially in `n`. Under negligible +MLWE and SelfTargetMSIS advantage families this makes the EUF-CMA advantage family negligible. + +A fixed-scheme wrapper would be degenerate: with a *constant* `ε > 0` the loss term +`2·qS·(qH+1)·ε/(1−p)` is only negligible when the query budgets vanish. Here the slacks are +themselves negligible families, so each loss term is `poly(n) · negligible(n)`, which is negligible +by `negligible_polynomial_mul`. -/ + +omit nttOps in +/-- A geometric family `r ^ n` with `0 ≤ r < 1` is negligible (after `ENNReal.ofReal`): for every +power `k`, `n ^ k · r ^ n → 0` (`tendsto_pow_const_mul_const_pow_of_lt_one`), and `ENNReal.ofReal` +is continuous. This provides concrete negligible slack/advantage families for the non-vacuity +witness `asymptotic_loss_regime_satisfiable`. -/ +theorem negligible_ofReal_geometric (r : ℝ) (hr0 : 0 ≤ r) (hr1 : r < 1) : + negligible (fun n => ENNReal.ofReal (r ^ n)) := by + intro k + have hreal : Filter.Tendsto (fun n : ℕ => (n : ℝ) ^ k * r ^ n) Filter.atTop (nhds 0) := + tendsto_pow_const_mul_const_pow_of_lt_one k hr0 hr1 + have h2 : Filter.Tendsto (fun n : ℕ => ENNReal.ofReal ((n : ℝ) ^ k * r ^ n)) Filter.atTop + (nhds (ENNReal.ofReal 0)) := + (ENNReal.continuous_ofReal.tendsto 0).comp hreal + rw [ENNReal.ofReal_zero] at h2 + refine h2.congr (fun n => ?_) + rw [ENNReal.ofReal_mul (by positivity), ENNReal.ofReal_pow (by positivity), + ENNReal.ofReal_natCast] + +omit nttOps in +/-- Building block: a fixed-constant multiple of `qS ^ dS · qH ^ dH · slack n` is negligible +whenever `qS`, `qH` are polynomially bounded and `slack` is a negligible (real-valued) family. The +product is bounded above by `(poly evaluation) · (constant) · ofReal (slack n)`; the polynomial +absorbs the query powers and the negligible slack drives the product to `0` faster than any +polynomial via `negligible_polynomial_mul`. -/ +private theorem negl_poly_slack + (qS qH : ℕ → ℕ) (slack : ℕ → ℝ) (c : ℝ) (hc : 0 ≤ c) + (pS pH : Polynomial ℕ) (dS dH : ℕ) + (hqS : ∀ n, qS n ≤ pS.eval n) (hqH : ∀ n, qH n ≤ pH.eval n) + (hslackneg : negligible (fun n => ENNReal.ofReal (slack n))) : + negligible (fun n => ENNReal.ofReal (c * (qS n) ^ dS * (qH n) ^ dH * slack n)) := by + have hbound : ∀ n, ENNReal.ofReal (c * (qS n) ^ dS * (qH n) ^ dH * slack n) ≤ + (↑((pS.eval n) ^ dS * (pH.eval n) ^ dH) : ℝ≥0∞) * + (ENNReal.ofReal c * ENNReal.ofReal (slack n)) := by + intro n + rcases le_or_gt 0 (slack n) with hs | hs + · rw [show c * (qS n : ℝ) ^ dS * (qH n : ℝ) ^ dH * slack n + = ((qS n : ℝ) ^ dS * (qH n : ℝ) ^ dH) * (c * slack n) by ring, + ENNReal.ofReal_mul (by positivity), ENNReal.ofReal_mul hc] + gcongr + rw [show ((qS n : ℝ) ^ dS * (qH n : ℝ) ^ dH) + = ((((qS n) ^ dS) * ((qH n) ^ dH) : ℕ) : ℝ) by push_cast; ring, + ENNReal.ofReal_natCast] + exact_mod_cast Nat.mul_le_mul (Nat.pow_le_pow_left (hqS n) dS) + (Nat.pow_le_pow_left (hqH n) dH) + · have hle : c * (qS n : ℝ) ^ dS * (qH n : ℝ) ^ dH * slack n ≤ 0 := by + have hpos : (0 : ℝ) ≤ c * (qS n : ℝ) ^ dS * (qH n : ℝ) ^ dH := by positivity + nlinarith + rw [ENNReal.ofReal_of_nonpos hle]; exact zero_le + refine negligible_of_le hbound ?_ + have hconst : negligible (fun n => ENNReal.ofReal c * ENNReal.ofReal (slack n)) := + negligible_const_mul hslackneg ENNReal.ofReal_ne_top + have hpoly := negligible_polynomial_mul hconst (pS ^ dS * pH ^ dH) + refine negligible_of_le (fun n => ?_) hpoly + rw [Polynomial.eval_mul, Polynomial.eval_pow, Polynomial.eval_pow] + +omit nttOps in +/-- **The CMA-to-NMA statistical loss is a negligible family** when the abort rate `p_abort` is a +fixed constant `< 1`, the signing / hashing budgets `qS`, `qH` are polynomially bounded, and the +three per-key slacks `ε` (commitment guessing), `ζ_zk` (HVZK), and `δ` (key regularity) are +negligible families. Each of the four loss terms is a fixed-constant multiple of a polynomial in the +query budgets times a negligible slack, hence negligible by `negl_poly_slack`; the final `δ` term is +negligible by hypothesis. The total `cmaToNmaLoss` is bounded by their sum (subadditivity of +`ENNReal.ofReal`). -/ +theorem cmaToNmaLoss_negligible + (qS qH : ℕ → ℕ) (ε ζ_zk δ : ℕ → ℝ) (p_abort : ℝ) (hp : p_abort < 1) + (pS pH : Polynomial ℕ) + (hqS : ∀ n, qS n ≤ pS.eval n) (hqH : ∀ n, qH n ≤ pH.eval n) + (hεneg : negligible (fun n => ENNReal.ofReal (ε n))) + (hζneg : negligible (fun n => ENNReal.ofReal (ζ_zk n))) + (hδneg : negligible (fun n => ENNReal.ofReal (δ n))) : + negligible (fun n => ENNReal.ofReal + (FiatShamirWithAbort.cmaToNmaLoss (qS n) (qH n) (ε n) p_abort (ζ_zk n) (δ n) hp)) := by + have h1mp : (0 : ℝ) < 1 - p_abort := by linarith + have t1 := negl_poly_slack qS (fun n => qH n + 1) ε (2 / (1 - p_abort)) + (by positivity) pS (pH + 1) 1 1 hqS + (fun n => by simpa [Polynomial.eval_add] using Nat.add_le_add_right (hqH n) 1) hεneg + have t2 := negl_poly_slack (fun n => qS n * (qS n + 1)) qH ε (1 / (2 * (1 - p_abort) ^ 2)) + (by positivity) (pS * (pS + 1)) pH 1 0 + (fun n => by + rw [Polynomial.eval_mul, Polynomial.eval_add, Polynomial.eval_one] + exact Nat.mul_le_mul (hqS n) (Nat.add_le_add_right (hqS n) 1)) + (fun n => hqH n) hεneg + have t3 := negl_poly_slack qS qH ζ_zk (1 / (1 - p_abort)) (by positivity) pS pH 1 0 + hqS hqH hζneg + have hsum := negligible_add (negligible_add (negligible_add t1 t2) t3) hδneg + refine negligible_of_le (g := fun n => + ENNReal.ofReal (2 / (1 - p_abort) * (qS n : ℝ) ^ 1 * ((qH n + 1 : ℕ) : ℝ) ^ 1 * ε n) + + ENNReal.ofReal (1 / (2 * (1 - p_abort) ^ 2) * ((qS n * (qS n + 1) : ℕ) : ℝ) ^ 1 * + (qH n : ℝ) ^ 0 * ε n) + + ENNReal.ofReal (1 / (1 - p_abort) * (qS n : ℝ) ^ 1 * (qH n : ℝ) ^ 0 * ζ_zk n) + + ENNReal.ofReal (δ n)) (fun n => ?_) hsum + have heq : (FiatShamirWithAbort.cmaToNmaLoss (qS n) (qH n) (ε n) p_abort (ζ_zk n) (δ n) hp) + = (2 / (1 - p_abort) * (qS n : ℝ) ^ 1 * ((qH n + 1 : ℕ) : ℝ) ^ 1 * ε n) + + (1 / (2 * (1 - p_abort) ^ 2) * ((qS n * (qS n + 1) : ℕ) : ℝ) ^ 1 * + (qH n : ℝ) ^ 0 * ε n) + + (1 / (1 - p_abort) * (qS n : ℝ) ^ 1 * (qH n : ℝ) ^ 0 * ζ_zk n) + δ n := by + rw [FiatShamirWithAbort.cmaToNmaLoss]; push_cast; field_simp + rw [heq] + calc ENNReal.ofReal (_ + _ + _ + δ n) + ≤ ENNReal.ofReal (_ + _ + _) + ENNReal.ofReal (δ n) := ENNReal.ofReal_add_le + _ ≤ _ + ENNReal.ofReal _ + ENNReal.ofReal (δ n) := by gcongr; exact ENNReal.ofReal_add_le + _ ≤ ENNReal.ofReal _ + ENNReal.ofReal _ + ENNReal.ofReal _ + ENNReal.ofReal (δ n) := by + gcongr; exact ENNReal.ofReal_add_le + +omit nttOps in +/-- **Asymptotic (negligible) EUF-CMA security of ML-DSA in the idealized short-key model.** + +The security-parameter-indexed, non-degenerate headline. The ML-DSA scheme is given as a +*family* `(p n, prims n)` over the security parameter `n`, with all carrier instances +supplied per `n`. The hypotheses are the `n`-indexed lifts of those of +`euf_cma_security_of_nma_short`, plus: + +* polynomial query bounds `qS n ≤ pS.eval n`, `qH n ≤ pH.eval n`; +* negligible commitment-guessing slack `ε`, key-regularity slack `δ`, HVZK slack `ζ_zk`, + and MLWE-bridge slack `εbridge` families (the commitment / response spaces grow with `n`); +* negligible MLWE and SelfTargetMSIS advantage families `mlweAdv`, `stmsisAdv` dominating + every reduction adversary (the hardness assumptions, carried as `n`-indexed families per + the standard ROM model). + +The conclusion is that the EUF-CMA advantage family of `adv` is negligible. The proof +instantiates the per-`n` bound `euf_cma_security_of_nma_short`, dominates the two existential +reductions by their negligible families, and bounds the statistical loss family with +`cmaToNmaLoss_negligible`: with polynomially-bounded queries and negligible slacks each loss +term is `poly(n) · negligible(n)`. + +No cost model is attached: the statement quantifies over unrestricted adversaries and +`n`-indexed advantage families, not over poly-time adversaries (see the scope note in the +module docstring). The numerical regime is jointly satisfiable with genuinely growing query +budgets (`asymptotic_loss_regime_satisfiable`). -/ +theorem euf_cma_security_asymptotic_short + (p' : ℕ → Params) (prims' : ∀ n, Primitives (p' n)) [nttOps' : NTTRingOps] + (instHigh : ∀ n, DecidableEq (prims' n).High) + {M' : Type} [DecidableEq M'] + (instCommEq : ∀ n, DecidableEq (Commitment (p' n) (prims' n))) + (instCommInh : ∀ n, Inhabited (Commitment (p' n) (prims' n))) + (instRespInh : ∀ n, Inhabited (Response (p' n) (prims' n))) + (instRql : ∀ n, SampleableType (RqVec (p' n).l)) + (instRqk : ∀ n, SampleableType (RqVec (p' n).k)) + (instChal : ∀ n, SampleableType (CommitHashBytes (p' n))) + (instPk : ∀ n, SampleableType (PublicKey (p' n) (prims' n))) + (mlwe : ∀ n, LearningWithErrors.Problem (TqMatrix (p' n).k (p' n).l) + (RqVec (p' n).l) (RqVec (p' n).k)) + (stmsis : ∀ n, SelfTargetMSIS.Problem + (TqMatrix (p' n).k (p' n).l) (Response (p' n) (prims' n)) + (PublicKey (p' n) (prims' n)) (M' × Commitment (p' n) (prims' n)) + (CommitHashBytes (p' n))) + (maxAttempts : ℕ → ℕ) + (hr : ∀ n, GenerableRelation (PublicKey (p' n) (prims' n)) (SecretKey (p' n)) + (validKeyPairShort (p' n) (prims' n))) + (hGen : ∀ n, (hr n).gen = keygenShort (p' n) (prims' n)) + (hStmsis : ∀ n, stmsis n = mldsaSTMSISShort (p' n) (prims' n) M') + (sim : ∀ n, PublicKey (p' n) (prims' n) → ProbComp + (Option (Commitment (p' n) (prims' n) × CommitHashBytes (p' n) × + Response (p' n) (prims' n)))) + (ζ_zk : ℕ → ℝ) (hζ : ∀ n, 0 ≤ ζ_zk n) + (hhvzk : ∀ n, (identificationSchemeShort (p' n) (prims' n)).HVZK (sim n) (ζ_zk n)) + (qS qH : ℕ → ℕ) (ε δ : ℕ → ℝ) (p_abort : ℝ) + (hp : p_abort < 1) (hp₀ : 0 ≤ p_abort) + (hε : ∀ n, 0 ≤ ε n) (hδ : ∀ n, 0 ≤ δ n) + (Good : ∀ n, PublicKey (p' n) (prims' n) → SecretKey (p' n) → Prop) + (hGood : ∀ n, Pr[ fun xw : PublicKey (p' n) (prims' n) × SecretKey (p' n) => + ¬ Good n xw.1 xw.2 | (hr n).gen] ≤ ENNReal.ofReal (δ n)) + (hGuess : ∀ n, ∀ pk sk, Good n pk sk → ∀ cm : Commitment (p' n) (prims' n), + Pr[= cm | Prod.fst <$> (identificationSchemeShort (p' n) (prims' n)).commit pk sk] ≤ + ENNReal.ofReal (ε n)) + (hAbort : ∀ n, ∀ pk sk, Good n pk sk → + Pr[= none | (identificationSchemeShort (p' n) (prims' n)).honestExecution pk sk] ≤ + ENNReal.ofReal p_abort) + (hAbortSim : ∀ n, ∀ pk sk, Good n pk sk → + Pr[= none | sim n pk] ≤ ENNReal.ofReal p_abort) + (adv : ∀ n, SignatureAlg.unforgeableAdv + (FiatShamirWithAbort (identificationSchemeShort (p' n) (prims' n)) (hr n) M' + (maxAttempts n))) + (hQ : ∀ n, ∀ pk, FiatShamir.signHashQueryBound M' + (S' := Option (Commitment (p' n) (prims' n) × Response (p' n) (prims' n))) + (oa := (adv n).main pk) (qS n) (qH n)) + (εbridge : ℕ → ℝ) + (hMlweBridge : ∀ n, ∀ (main : PublicKey (p' n) (prims' n) → + OracleComp (unifSpec + (M' × Commitment (p' n) (prims' n) →ₒ CommitHashBytes (p' n))) + (M' × Option (Commitment (p' n) (prims' n) × Response (p' n) (prims' n)))), + ∃ B : LearningWithErrors.Adversary (mlwe n), + LearningWithErrors.advantage (mldsaMLWEShort (p' n) (prims' n)) + (distinguisherBShort (p' n) (prims' n) (hr n) (maxAttempts n) main) ≤ + LearningWithErrors.advantage (mlwe n) B + εbridge n) + (pS pH : Polynomial ℕ) + (hqS : ∀ n, qS n ≤ pS.eval n) (hqH : ∀ n, qH n ≤ pH.eval n) + (mlweAdv stmsisAdv : ℕ → ℝ≥0∞) + (hmlweNegl : negligible mlweAdv) (hstmsisNegl : negligible stmsisAdv) + (hMlweBound : ∀ n (B : LearningWithErrors.Adversary (mlwe n)), + ENNReal.ofReal (LearningWithErrors.advantage (mlwe n) B) ≤ mlweAdv n) + (hStmsisBound : ∀ n (C : SelfTargetMSIS.Adversary (stmsis n)), + SelfTargetMSIS.advantage C ≤ stmsisAdv n) + (hbridgeNegl : negligible (fun n => ENNReal.ofReal (εbridge n))) + (hεneg : negligible (fun n => ENNReal.ofReal (ε n))) + (hδneg : negligible (fun n => ENNReal.ofReal (δ n))) + (hζneg : negligible (fun n => ENNReal.ofReal (ζ_zk n))) : + negligible (fun n => (adv n).advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment (p' n) (prims' n)) (Chal := CommitHashBytes (p' n)) M')) := by + have hbound : ∀ n, (adv n).advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment (p' n) (prims' n)) (Chal := CommitHashBytes (p' n)) M') ≤ + mlweAdv n + ENNReal.ofReal (εbridge n) + stmsisAdv n + + ENNReal.ofReal (FiatShamirWithAbort.cmaToNmaLoss (qS n) (qH n) (ε n) p_abort + (ζ_zk n) (δ n) hp) := by + intro n + obtain ⟨mlweRed, stmsisRed, hb⟩ := + @euf_cma_security_of_nma_short (p' n) (prims' n) nttOps' (instHigh n) M' _ + (instCommEq n) (instCommInh n) (instRespInh n) (instRql n) (instRqk n) + (instChal n) (instPk n) + (mlwe n) (stmsis n) (maxAttempts n) (hr n) (hGen n) (hStmsis n) + (sim n) (ζ_zk n) (hζ n) (hhvzk n) + (qS n) (qH n) (ε n) p_abort (δ n) (hε n) (hδ n) hp₀ hp (Good n) (hGood n) (hGuess n) + (hAbort n) (hAbortSim n) (adv n) (hQ n) (εbridge n) (hMlweBridge n) + refine le_trans hb ?_ + have h1 : ENNReal.ofReal (LearningWithErrors.advantage (mlwe n) mlweRed + εbridge n) ≤ + mlweAdv n + ENNReal.ofReal (εbridge n) := + le_trans ENNReal.ofReal_add_le (add_le_add (hMlweBound n mlweRed) le_rfl) + exact add_le_add (add_le_add h1 (hStmsisBound n stmsisRed)) le_rfl + refine negligible_of_le hbound ?_ + refine negligible_add (negligible_add (negligible_add hmlweNegl hbridgeNegl) hstmsisNegl) ?_ + exact cmaToNmaLoss_negligible qS qH ε ζ_zk δ p_abort hp pS pH hqS hqH hεneg hζneg hδneg + +omit nttOps in +/-- **Consistency of the asymptotic numerical-loss regime.** + +The quantitative hypotheses of `euf_cma_security_asymptotic_short` — *polynomially-bounded* +query budgets together with *negligible* statistical slacks (commitment guessing `ε`, HVZK +`ζ_zk`, key regularity `δ`, MLWE bridge `εbridge`) and negligible hardness advantage families +— are jointly satisfiable with query budgets that genuinely **grow** with the security +parameter. Concretely, taking `qS n = qH n = n` (bounded by `Polynomial.X`, i.e. *not* +vanishing), all slacks and advantage families equal to `(1 / 2) ^ n`, and `p_abort = 1 / 2`, +the resulting `cmaToNmaLoss` family, together with the two hardness families and the bridge +slack, is negligible — so the dominating sum in the headline's internal bound is negligible. + +This rules out the degenerate reading of the headline (where polynomial queries against a +*fixed* positive `ε` would force the budgets to vanish): here the budgets grow polynomially +while the loss still decays. + +This statement chooses **numerical sequences only**. It does not instantiate the hardness +problems, `hMlweBridge`, the HVZK simulator family, the scheme family, or the other +hypotheses of `euf_cma_security_asymptotic_short`; it describes the loss regime, not the +satisfiability of the security theorem. -/ +theorem asymptotic_loss_regime_satisfiable : + ∃ (qS qH : ℕ → ℕ) (ε ζ_zk δ εbridge : ℕ → ℝ) (p_abort : ℝ) (hp : p_abort < 1) + (pS pH : Polynomial ℕ) (mlweAdv stmsisAdv : ℕ → ℝ≥0∞), + (∀ n, qS n ≤ pS.eval n) ∧ (∀ n, qH n ≤ pH.eval n) ∧ + -- the queries genuinely grow (are not the degenerate vanishing-query regime) + (∀ n, qS n = n) ∧ (∀ n, qH n = n) ∧ + negligible mlweAdv ∧ negligible stmsisAdv ∧ + negligible (fun n => ENNReal.ofReal (ε n)) ∧ + negligible (fun n => ENNReal.ofReal (ζ_zk n)) ∧ + negligible (fun n => ENNReal.ofReal (δ n)) ∧ + negligible (fun n => ENNReal.ofReal (εbridge n)) ∧ + negligible (fun n => mlweAdv n + ENNReal.ofReal (εbridge n) + stmsisAdv n + + ENNReal.ofReal (FiatShamirWithAbort.cmaToNmaLoss (qS n) (qH n) (ε n) p_abort + (ζ_zk n) (δ n) hp)) := by + have hgrow : ∀ n : ℕ, n ≤ (Polynomial.X : Polynomial ℕ).eval n := fun n => by simp + have hneg : negligible (fun n => ENNReal.ofReal ((1 / 2 : ℝ) ^ n)) := + negligible_ofReal_geometric (1 / 2) (by norm_num) (by norm_num) + have hEeq : ∀ n : ℕ, (1 / 2 : ℝ≥0∞) ^ n = ENNReal.ofReal ((1 / 2 : ℝ) ^ n) := by + intro n + rw [ENNReal.ofReal_pow (by norm_num)] + congr 1 + rw [ENNReal.ofReal_div_of_pos (by norm_num)] + simp [ENNReal.ofReal_one] + have hnegE : negligible (fun n => (1 / 2 : ℝ≥0∞) ^ n) := by + simp only [hEeq]; exact hneg + refine ⟨fun n => n, fun n => n, fun n => (1 / 2) ^ n, fun n => (1 / 2) ^ n, + fun n => (1 / 2) ^ n, fun n => (1 / 2) ^ n, 1 / 2, by norm_num, Polynomial.X, Polynomial.X, + fun n => (1 / 2) ^ n, fun n => (1 / 2) ^ n, hgrow, hgrow, fun _ => rfl, fun _ => rfl, + hnegE, hnegE, hneg, hneg, hneg, hneg, ?_⟩ + refine negligible_add (negligible_add (negligible_add hnegE hneg) hnegE) ?_ + exact cmaToNmaLoss_negligible (fun n => n) (fun n => n) (fun n => (1 / 2) ^ n) + (fun n => (1 / 2) ^ n) (fun n => (1 / 2) ^ n) (1 / 2) (by norm_num) Polynomial.X Polynomial.X + hgrow hgrow hneg hneg hneg + +end Headline + /-! ## Status The live short-secret reduction and the extraction bound are fully proven: @@ -992,4 +2070,44 @@ The live short-secret reduction and the extraction bound are fully proven: `verify = true → isValid = true` closes the per-answer inequality). -/ +section MatrixHeadline + +variable (p : Params) (prims : Primitives p) [nttOps : NTTRingOps] + [DecidableEq prims.High] + {M : Type} [DecidableEq M] [DecidableEq (Commitment p prims)] + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + [SampleableType (RqVec p.l)] [SampleableType (RqVec p.k)] + [SampleableType (TqMatrix p.k p.l)] + [SampleableType (CommitHashBytes p)] + +/-- **The matrix-MLWE-facing NMA headline.** `nma_security_short` with the abstract-problem +bridge discharged: the MLWE leg lands on the standard uniform-matrix short-secret problem +`mldsaMatrixMLWE`, at the cost of one application of the `expandAIdealization` assumption +(`advantage_mldsaMLWEShort_le_matrix`, supplying the bridge slack `εA`). The SelfTargetMSIS leg +still lands on the *tailored* `mldsaSTMSISShort`, not the standard SelfTargetMSIS normal form — +that second bridge is follow-up work, so only the MLWE side is stated against a standard +literature problem here. No caller-supplied inequality remains: every hypothesis is a +satisfiable pinned equality (`keygenShort_generable`), a proven reduction, or the named XOF +idealization. -/ +theorem nma_security_short_matrix (maxAttempts : ℕ) (εA : ℝ) + (hA : NMA.expandAIdealization p prims εA) + (hr : GenerableRelation (PublicKey p prims) (SecretKey p) (validKeyPairShort p prims)) + (hGen : hr.gen = NMA.keygenShort p prims) : + ∀ (adv : SignatureAlg.eufNmaAdv + (FiatShamirWithAbort (identificationSchemeShort p prims) hr M maxAttempts)), + ∃ (mlweReduction : LearningWithErrors.Adversary (NMA.mldsaMatrixMLWE p)) + (stmsisReduction : SelfTargetMSIS.Adversary (NMA.mldsaSTMSISShort p prims M)), + adv.advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) ≤ + ENNReal.ofReal + (LearningWithErrors.advantage (NMA.mldsaMatrixMLWE p) mlweReduction + εA) + + SelfTargetMSIS.advantage stmsisReduction := + nma_security_short p prims (NMA.mldsaMatrixMLWE p) (NMA.mldsaSTMSISShort p prims M) + maxAttempts hr hGen rfl εA + (fun main => ⟨NMA.matrixLift p prims (NMA.distinguisherBShort p prims hr maxAttempts main), + NMA.advantage_mldsaMLWEShort_le_matrix p prims hA _⟩) + +end MatrixHeadline + end MLDSA diff --git a/LatticeCryptoTest.lean b/LatticeCryptoTest.lean index 75d3231ba..9922b3031 100644 --- a/LatticeCryptoTest.lean +++ b/LatticeCryptoTest.lean @@ -8,5 +8,6 @@ public import LatticeCryptoTest.Falcon.Helpers public import LatticeCryptoTest.Falcon.TestVectors public import LatticeCryptoTest.MLDSA.ACVPVectors public import LatticeCryptoTest.MLDSA.Helpers +public import LatticeCryptoTest.MLDSA.NonVacuity public import LatticeCryptoTest.MLKEM.ACVPVectors public import LatticeCryptoTest.MLKEM.Helpers diff --git a/LatticeCryptoTest/MLDSA/NonVacuity.lean b/LatticeCryptoTest/MLDSA/NonVacuity.lean new file mode 100644 index 000000000..b1398ad86 --- /dev/null +++ b/LatticeCryptoTest/MLDSA/NonVacuity.lean @@ -0,0 +1,246 @@ +/- +Copyright (c) 2026 Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Oleksandr Vovkotrub +-/ + +module +public import LatticeCrypto.MLDSA.SecurityNMA + +/-! +# ML-DSA short-model CMA headline: joint hypothesis-consistency witness + +`MLDSA.euf_cma_security_of_nma_short` is a conditional theorem. Its hypothesis frontier bundles +the hardness-problem pins (`hGen`, `hStmsis`), the HVZK simulator data (`sim`, `ζ_zk`, `hζ`, +`hhvzk`), the good-key package (`Good`, `hGood`, `hGuess`, `hAbort`, `hAbortSim` at some +`p_abort < 1`), the query bounds (`qS`, `qH`, `hQ`) on a CMA adversary, and the MLWE bridge +(`εbridge`, `hMlweBridge`). A conditional theorem asserts nothing if its hypotheses are jointly +uninhabitable; this file rules that out by discharging every hypothesis **simultaneously**, at +arbitrary parameters `(p, prims)` under the same carrier instances the headline itself assumes, +and applying the headline end to end to a concrete trivial adversary +(`trivial_euf_cma_security_of_nma_short`). + +This is a **logical consistency (inhabitance) witness only**: the trivial budgets (`ε = 1`, +`ζ_zk = 1`, `δ = 1`, `εbridge = 1` via `expandAIdealization` at the vacuous bound `εA = 1`), the +no-query forger, and the never-aborting simulator carry no quantitative security content, and no +security claim about any real ML-DSA parameter set follows from it. + +The witness values are: + +* `hr := hrShort`, `hGen := rfl` — the genuine short-key generable relation + (`keygenShort_generable`); +* `stmsis := mldsaSTMSISShort`, `hStmsis := rfl` — the pinned SelfTargetMSIS problem; +* `sim := neverAbortSim` (constant non-`none` transcript) with `ζ_zk = 1`, discharged by + `tvDist_le_one`; +* `Good := honestNoAbortGood` — the pairs at which the honest prover never aborts — with + `p_abort = 0`: `hAbort` holds by definition of the event and `hAbortSim` because the simulator + never aborts; `hGood` holds at the trivial regularity budget `δ = 1`. Whether + `honestNoAbortGood` is satisfiable at a given parameter set is a quantitative completeness + question (cf. `idsWithAbort_complete`) that the frontier does not require; +* `ε = 1` for the commitment-guessing bound (`probOutput_le_one`); +* the trivial forger `trivialForger` (no queries, immediately returned `none` forgery) with + `qS = qH = 0`; +* `mlwe := mldsaMatrixMLWE`, `εbridge = 1`: the proven seed-to-matrix reduction + `advantage_mldsaMLWEShort_le_matrix` under `expandAIdealization p prims 1`, which holds + unconditionally (`expandAIdealization_one`) since advantages are differences of probabilities. +-/ + +@[expose] public section + +open OracleComp OracleSpec ENNReal +open LatticeCrypto TransformOps + +namespace MLDSA + +open NMA + +section ShortCMAWitness + +variable (p : Params) (prims : Primitives p) [nttOps : NTTRingOps] + [DecidableEq prims.High] + (M : Type) [DecidableEq M] [Inhabited M] [DecidableEq (Commitment p prims)] + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] + [SampleableType (RqVec p.l)] [SampleableType (RqVec p.k)] + [SampleableType (TqMatrix p.k p.l)] + [SampleableType (CommitHashBytes p)] + [SampleableType (PublicKey p prims)] + +/-! ## The witness data: simulator, good event, and trivial adversary -/ + +/-- The never-aborting HVZK simulator: output the default transcript with probability one. It +discharges `hAbortSim` at `p_abort = 0` (`probOutput_none_neverAbortSim`) and the HVZK hypothesis +at the trivial budget `ζ_zk = 1` (`neverAbortSim_hvzk`); it carries no zero-knowledge content. -/ +noncomputable def neverAbortSim : + PublicKey p prims → + ProbComp (Option (Commitment p prims × CommitHashBytes p × Response p prims)) := + fun _ => pure (some default) + +/-- The good-key event of the witness: the pairs at which the honest prover never aborts. This +is the intended semantics of the `Good` gate in the CMA-to-NMA reduction; `hAbort` at +`p_abort = 0` holds definitionally on this event, and `hGood` holds at the trivial regularity +budget `δ = 1`. The frontier does not require the event to be satisfiable; establishing that at +a concrete parameter set is a quantitative completeness fact, not a consistency one. -/ +def honestNoAbortGood : PublicKey p prims → SecretKey p → Prop := + fun pk sk => + Pr[= none | (identificationSchemeShort p prims).honestExecution pk sk] = 0 + +/-- The trivial CMA forger against the short-model Fiat-Shamir-with-aborts scheme: make no +oracle queries and immediately return the aborted forgery `(default, none)`. It witnesses the +query-bound hypothesis at `qS = qH = 0` (`trivialForger_signHashQueryBound`). -/ +noncomputable def trivialForger (maxAttempts : ℕ) : + SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p))) + (identificationSchemeShort p prims) (hrShort p prims) M maxAttempts) where + main := fun _ => pure (default, none) + +/-! ## Per-hypothesis discharges -/ + +omit [DecidableEq (Commitment p prims)] [SampleableType (RqVec p.k)] + [SampleableType (TqMatrix p.k p.l)] [SampleableType (PublicKey p prims)] in +/-- `hhvzk` at the trivial budget: any simulator is a `ζ_zk = 1` HVZK simulator, since total +variation distance never exceeds one. -/ +lemma neverAbortSim_hvzk : + (identificationSchemeShort p prims).HVZK (neverAbortSim p prims) 1 := + fun _ _ _ => tvDist_le_one _ _ + +omit nttOps [DecidableEq prims.High] [DecidableEq (Commitment p prims)] + [SampleableType (RqVec p.l)] [SampleableType (RqVec p.k)] [SampleableType (TqMatrix p.k p.l)] + [SampleableType (CommitHashBytes p)] [SampleableType (PublicKey p prims)] in +/-- The never-aborting simulator indeed never aborts: the probability of `none` is zero. -/ +lemma probOutput_none_neverAbortSim (pk : PublicKey p prims) : + Pr[= none | neverAbortSim p prims pk] = 0 := + probOutput_eq_zero_of_not_mem_support (by simp [neverAbortSim, support_pure]) + +omit [DecidableEq M] [DecidableEq (Commitment p prims)] [Inhabited (Commitment p prims)] + [Inhabited (Response p prims)] [SampleableType (TqMatrix p.k p.l)] + [SampleableType (CommitHashBytes p)] [SampleableType (PublicKey p prims)] in +/-- The trivial forger makes no signing and no random-oracle queries. -/ +lemma trivialForger_signHashQueryBound (maxAttempts : ℕ) (pk : PublicKey p prims) : + FiatShamir.signHashQueryBound M + (S' := Option (Commitment p prims × Response p prims)) + (oa := (trivialForger p prims M maxAttempts).main pk) 0 0 := + ⟨isQueryBoundP_pure _ _ _, isQueryBoundP_pure _ _ _⟩ + +omit nttOps [DecidableEq prims.High] [DecidableEq (Commitment p prims)] + [Inhabited (Commitment p prims)] [Inhabited (Response p prims)] [SampleableType (RqVec p.l)] + [SampleableType (RqVec p.k)] [SampleableType (CommitHashBytes p)] + [SampleableType (PublicKey p prims)] in +/-- `expandAIdealization` holds unconditionally at the trivial budget `εA = 1`: the two branch +probabilities both lie in `[0, 1]`, so their difference is at most one in absolute value. This +carries no idealization content; it exists to discharge the MLWE bridge of the consistency +witness through the proven reduction `advantage_mldsaMLWEShort_le_matrix`. -/ +lemma expandAIdealization_one : expandAIdealization p prims 1 := by + intro _ D + have key : ∀ x y : ProbComp Bool, + |(Pr[= true | x]).toReal - (Pr[= true | y]).toReal| ≤ 1 := fun x y => by + have hx1 : (Pr[= true | x]).toReal ≤ 1 := + le_trans (ENNReal.toReal_mono ENNReal.one_ne_top probOutput_le_one) + (le_of_eq ENNReal.toReal_one) + have hy1 : (Pr[= true | y]).toReal ≤ 1 := + le_trans (ENNReal.toReal_mono ENNReal.one_ne_top probOutput_le_one) + (le_of_eq ENNReal.toReal_one) + have hx0 : (0 : ℝ) ≤ (Pr[= true | x]).toReal := ENNReal.toReal_nonneg + have hy0 : (0 : ℝ) ≤ (Pr[= true | y]).toReal := ENNReal.toReal_nonneg + rw [abs_sub_le_iff] + exact ⟨by linarith, by linarith⟩ + exact key _ _ + +/-! ## The joint frontier certificate -/ + +open scoped Classical in +omit [SampleableType (PublicKey p prims)] in +/-- **Joint consistency (inhabitance) witness for the `MLDSA.euf_cma_security_of_nma_short` +hypotheses.** At arbitrary parameters `(p, prims)` (under the headline's own carrier +instances), any message type `M`, and any retry budget, every explicit hypothesis of the +short-model CMA headline holds simultaneously at the witness values listed in the module +docstring: the pinned generable relation and SelfTargetMSIS problem (`hGen`/`hStmsis`), the +HVZK package at `ζ_zk = 1`, the nonnegativity side conditions with `p_abort = 0 < 1`, the +good-key package (`hGood` at `δ = 1`, `hGuess` at `ε = 1`, `hAbort`/`hAbortSim` at +`p_abort = 0`), the query bounds `qS = qH = 0` on the trivial forger, and the MLWE bridge at +`εbridge = 1` through the proven seed-to-matrix reduction. The hypothesis conjunction of the +headline is therefore jointly inhabitable; this witness carries no quantitative security +content. -/ +theorem mldsa_short_cma_hyps_inhabited (maxAttempts : ℕ) : + -- hGen + (hrShort p prims).gen = keygenShort p prims ∧ + -- hStmsis (the pinned SelfTargetMSIS problem) + mldsaSTMSISShort p prims M = mldsaSTMSISShort p prims M ∧ + -- hζ and hhvzk at ζ_zk = 1 + ((0 : ℝ) ≤ 1 ∧ (identificationSchemeShort p prims).HVZK (neverAbortSim p prims) 1) ∧ + -- hε, hδ, hp₀, hp at ε = 1, δ = 1, p_abort = 0 + ((0 : ℝ) ≤ 1 ∧ (0 : ℝ) ≤ 1 ∧ (0 : ℝ) ≤ 0 ∧ (0 : ℝ) < 1) ∧ + -- hGood at δ = 1 + (Pr[ fun xw : PublicKey p prims × SecretKey p => + ¬ honestNoAbortGood p prims xw.1 xw.2 | (hrShort p prims).gen] ≤ + ENNReal.ofReal 1) ∧ + -- hGuess at ε = 1 + (∀ pk sk, honestNoAbortGood p prims pk sk → ∀ cm : Commitment p prims, + Pr[= cm | Prod.fst <$> (identificationSchemeShort p prims).commit pk sk] ≤ + ENNReal.ofReal 1) ∧ + -- hAbort at p_abort = 0 + (∀ pk sk, honestNoAbortGood p prims pk sk → + Pr[= none | (identificationSchemeShort p prims).honestExecution pk sk] ≤ + ENNReal.ofReal 0) ∧ + -- hAbortSim at p_abort = 0 + (∀ pk sk, honestNoAbortGood p prims pk sk → + Pr[= none | neverAbortSim p prims pk] ≤ ENNReal.ofReal 0) ∧ + -- hQ at qS = qH = 0 + (∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commitment p prims × Response p prims)) + (oa := (trivialForger p prims M maxAttempts).main pk) 0 0) ∧ + -- hMlweBridge at mlwe = mldsaMatrixMLWE p, εbridge = 1 + (∀ main : PublicKey p prims → + OracleComp (unifSpec + (M × Commitment p prims →ₒ CommitHashBytes p)) + (M × Option (Commitment p prims × Response p prims)), + ∃ B : LearningWithErrors.Adversary (mldsaMatrixMLWE p), + LearningWithErrors.advantage (mldsaMLWEShort p prims) + (distinguisherBShort p prims (hrShort p prims) maxAttempts main) ≤ + LearningWithErrors.advantage (mldsaMatrixMLWE p) B + 1) := + ⟨rfl, rfl, ⟨zero_le_one, neverAbortSim_hvzk p prims⟩, + ⟨zero_le_one, zero_le_one, le_rfl, zero_lt_one⟩, + by rw [ENNReal.ofReal_one]; exact probEvent_le_one, + fun _pk _sk _h _cm => by rw [ENNReal.ofReal_one]; exact probOutput_le_one, + fun _pk _sk h => by rw [ENNReal.ofReal_zero]; exact h.le, + fun pk _sk _h => by + rw [ENNReal.ofReal_zero]; exact (probOutput_none_neverAbortSim p prims pk).le, + trivialForger_signHashQueryBound p prims M maxAttempts, + fun main => + ⟨matrixLift p prims (distinguisherBShort p prims (hrShort p prims) maxAttempts main), + advantage_mldsaMLWEShort_le_matrix p prims (expandAIdealization_one p prims) _⟩⟩ + +open scoped Classical in +/-- **End-to-end applicability of the short-model CMA headline.** The derived theorem +`MLDSA.euf_cma_security_of_nma_short` applies to the trivial forger with every hypothesis +discharged at the witness values of `mldsa_short_cma_hyps_inhabited`, producing its reductions +and bound. Consistency-only: with `ε = ζ_zk = δ = εbridge = 1` the resulting bound is trivial, +and no quantitative claim about any real ML-DSA parameter set follows. -/ +theorem trivial_euf_cma_security_of_nma_short (maxAttempts : ℕ) : + ∃ (mlweReduction : LearningWithErrors.Adversary (mldsaMatrixMLWE p)) + (stmsisReduction : SelfTargetMSIS.Adversary (mldsaSTMSISShort p prims M)), + (trivialForger p prims M maxAttempts).advantage + (FiatShamirWithAbort.runtime + (Commit := Commitment p prims) (Chal := CommitHashBytes p) M) ≤ + ENNReal.ofReal + (LearningWithErrors.advantage (mldsaMatrixMLWE p) mlweReduction + 1) + + SelfTargetMSIS.advantage stmsisReduction + + ENNReal.ofReal (FiatShamirWithAbort.cmaToNmaLoss 0 0 1 0 1 1 zero_lt_one) := + euf_cma_security_of_nma_short p prims (mldsaMatrixMLWE p) (mldsaSTMSISShort p prims M) + maxAttempts (hrShort p prims) rfl rfl (neverAbortSim p prims) 1 zero_le_one + (neverAbortSim_hvzk p prims) 0 0 1 0 1 zero_le_one zero_le_one le_rfl zero_lt_one + (honestNoAbortGood p prims) + (by rw [ENNReal.ofReal_one]; exact probEvent_le_one) + (fun _pk _sk _h _cm => by rw [ENNReal.ofReal_one]; exact probOutput_le_one) + (fun _pk _sk h => by rw [ENNReal.ofReal_zero]; exact h.le) + (fun pk _sk _h => by + rw [ENNReal.ofReal_zero]; exact (probOutput_none_neverAbortSim p prims pk).le) + (trivialForger p prims M maxAttempts) + (trivialForger_signHashQueryBound p prims M maxAttempts) + 1 + (fun main => + ⟨matrixLift p prims (distinguisherBShort p prims (hrShort p prims) maxAttempts main), + advantage_mldsaMLWEShort_le_matrix p prims (expandAIdealization_one p prims) _⟩) + +end ShortCMAWitness + +end MLDSA diff --git a/VCVio.lean b/VCVio.lean index 4bce7595e..3ce418091 100644 --- a/VCVio.lean +++ b/VCVio.lean @@ -27,7 +27,23 @@ public import VCVio.CryptoFoundations.FiatShamir.Sigma.Stateful.Spec public import VCVio.CryptoFoundations.FiatShamir.WithAbort public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Cost public import VCVio.CryptoFoundations.FiatShamir.WithAbort.ExpectedCost +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Bodies +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.BodyBounds +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.GhostLayer +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.NMAHandler +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Projections public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.Assembly +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.BodyHops +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.CouplingEngine +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.GhostReadCharge +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.HiddenReadFold +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.HopLemmas +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.Loss +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.NMAReduction +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.ReadRecording +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.TapeFactorization public import VCVio.CryptoFoundations.Fischlin public import VCVio.CryptoFoundations.Fischlin.Completeness public import VCVio.CryptoFoundations.Fischlin.CostAccounting diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies.lean new file mode 100644 index 000000000..daa962ab0 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies.lean @@ -0,0 +1,47 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Bodies +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.GhostLayer +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Projections +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.BodyBounds +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.NMAHandler + +/-! +# Hybrid signing bodies and ghost-layer machinery for Fiat-Shamir with aborts + +Cache-level signing bodies of the CMA-to-NMA hybrid chain for the +Fiat-Shamir-with-aborts transform (`realSignBody`, `progSignBody`, +`transSignBody`, `simSignBody`), together with the run-level hybrid handlers +(`hybridBaseImpl`, `hybridSignImpl`) and the ghost-layer presentation of the +reprogramming bodies used by the Prog → Trans hop: + +* `ghostSignBody` acts on a two-layer cache, writing accepted transcripts to + the real layer and rejected-attempt programmings to the ghost layer; the + projections `run_ghostSignBody_overlay` and `run_ghostSignBody_fst` recover + `progSignBody` and the accepted-only programming loop of `transSignBody`. +* `ghostHybridImpl` instruments the adversary's oracles over the layered cache + with a monotone bad flag firing on adversarial reads of the ghost layer, + with per-step projections onto both hybrid games + (`ghostHybridImpl_proj_prog`, `ghostHybridImpl_proj_trans`) and the + ghost-domain invariant `ghostHybridImpl_preserves_signed_inv`. + +The hybrid experiment itself and the hop lemmas live in +`FiatShamir.WithAbort.Security`. + +## Module layout + +The development is split along its phases: `Bodies` (retry loops and the four +signing bodies), `GhostLayer` (the two-layer cache presentation and the +ghost-instrumented handlers), `Projections` (projections onto both hybrid +games and the ghost-domain invariant), `BodyBounds` (the body-level collision +and deferred-sampling bounds), and `NMAHandler` (the layered ghost-tagged NMA +handler). This umbrella module re-exports all of them. +-/ + +@[expose] public section diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Bodies.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Bodies.lean new file mode 100644 index 000000000..283cd2335 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Bodies.lean @@ -0,0 +1,194 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort + +/-! +# Ghost-layer machinery for Fiat-Shamir with aborts: Bodies + +The first-success retry loops (`firstSome` and its distribution laws) and +the cache-level signing bodies of the CMA-to-NMA hybrid chain (`realSignBody`, +`progSignBody`, `transSignBody`, `simSignBody`). + +Part of the hybrid signing-body development for the CMA-to-NMA reduction; +`VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies` re-exports all of +its modules and holds the overview docstring. +-/ + +@[expose] public section + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) + +/-! ## First-success retry loops + +The real, reprogrammed, and simulated signing oracles of the hybrid chain all share the +same restart structure: iterate an optional sampler until the first non-`none` result, +up to a fixed attempt budget. `firstSome` abstracts that loop so the zero-knowledge hop +can be reduced to a single distributional lemma about retry loops. -/ + +/-- Iterate an optional sampler up to `n` times, returning the first non-`none` result +(or `none` when every attempt fails). -/ +def firstSome {α : Type} (attempt : ProbComp (Option α)) : ℕ → ProbComp (Option α) + | 0 => pure none + | n + 1 => do + match ← attempt with + | some a => pure (some a) + | none => firstSome attempt n + +lemma firstSome_succ {α : Type} (attempt : ProbComp (Option α)) (n : ℕ) : + firstSome attempt (n + 1) = + attempt >>= fun r => + match r with + | some a => pure (some a) + | none => firstSome attempt n := rfl + +/-- Gluing per-attempt simulation across a first-success retry loop: if two optional +samplers are within total-variation distance `ζ` and the second aborts with probability +at most `q`, then the `n`-attempt retry loops are within `ζ * (1 + q + ⋯ + q^(n-1))`. +In particular, with `q < 1` the loop simulation error is at most `ζ / (1 - q)`, +independently of the attempt budget. + +This is the distributional core of the `transSignBody`-to-`simSignBody` hop: each +hybrid step couples one more attempt, and attempt `j` is only reached when the first +`j` attempts of the second loop all abort. -/ +lemma tvDist_firstSome_le_geometric {α : Type} (a₁ a₂ : ProbComp (Option α)) + {ζ q : ℝ} (hζ : tvDist a₁ a₂ ≤ ζ) (hq : Pr[= none | a₂].toReal ≤ q) (hq0 : 0 ≤ q) : + ∀ n : ℕ, tvDist (firstSome a₁ n) (firstSome a₂ n) ≤ ζ * ∑ j ∈ Finset.range n, q ^ j + | 0 => by simp [firstSome] + | (n + 1) => by + have hζ0 : 0 ≤ ζ := le_trans (tvDist_nonneg a₁ a₂) hζ + have ih := tvDist_firstSome_le_geometric a₁ a₂ hζ hq hq0 n + have hGeomNonneg : (0 : ℝ) ≤ ∑ j ∈ Finset.range n, q ^ j := + Finset.sum_nonneg fun j _ => pow_nonneg hq0 j + set k₁ : Option α → ProbComp (Option α) := fun r => + match r with + | some a => pure (some a) + | none => firstSome a₁ n with hk₁ + set k₂ : Option α → ProbComp (Option α) := fun r => + match r with + | some a => pure (some a) + | none => firstSome a₂ n with hk₂ + have hterm : ∀ b : Option α, b ≠ (none : Option α) → + Pr[= b | a₂].toReal * tvDist (k₁ b) (k₂ b) = 0 := by + intro b hb + match b, hb with + | some a, _ => simp [hk₁, hk₂] + have hStep : tvDist (a₂ >>= k₁) (a₂ >>= k₂) ≤ + Pr[= none | a₂].toReal * tvDist (firstSome a₁ n) (firstSome a₂ n) := by + refine le_trans (tvDist_bind_left_le a₂ k₁ k₂) (le_of_eq ?_) + rw [tsum_eq_single (none : Option α) hterm] + calc + tvDist (firstSome a₁ (n + 1)) (firstSome a₂ (n + 1)) + = tvDist (a₁ >>= k₁) (a₂ >>= k₂) := by + rw [firstSome_succ, firstSome_succ] + _ ≤ tvDist (a₁ >>= k₁) (a₂ >>= k₁) + tvDist (a₂ >>= k₁) (a₂ >>= k₂) := + tvDist_triangle _ _ _ + _ ≤ ζ + Pr[= none | a₂].toReal * tvDist (firstSome a₁ n) (firstSome a₂ n) := + add_le_add (le_trans (tvDist_bind_right_le k₁ a₁ a₂) hζ) hStep + _ ≤ ζ + q * (ζ * ∑ j ∈ Finset.range n, q ^ j) := + add_le_add le_rfl (mul_le_mul hq ih (tvDist_nonneg _ _) hq0) + _ = ζ * ∑ j ∈ Finset.range (n + 1), q ^ j := by + have hsum : ∑ j ∈ Finset.range (n + 1), q ^ j = + q * (∑ j ∈ Finset.range n, q ^ j) + 1 := by + rw [Finset.sum_range_succ', Finset.mul_sum] + simp [pow_succ'] + rw [hsum] + ring + +/-! ## The hybrid signing bodies + +All four hybrid games run the adversary against the same uniform-sampling and +random-oracle handlers, differing only in the signing-oracle body. Each body is a +cache-level state transformer on the random-oracle cache. Following Fig. 2 of the +paper (adapted to the bounded restart loop): + +- `realSignBody` (Sign): the real signing loop, hashing each attempt's commitment + through the caching random oracle. Aborted attempts also populate the cache. +- `progSignBody` (Prog): every attempt **overwrites** the cache at `(msg, w)` with a + fresh uniform challenge, for rejected and accepted attempts alike, removing the + dependency between the cached challenge and the accept event. +- `transSignBody` (Trans): the loop runs privately on `ids.honestExecution` (no cache + interaction), and only the accepted transcript is programmed into the cache. +- `simSignBody` (Sim): as `transSignBody` with the per-attempt HVZK simulator in place + of the honest execution; the secret key is no longer used. -/ + +/-- Real signing-oracle body: the cache-level semantics of `fsAbortSignLoop` under the +caching random oracle. Each attempt queries the random oracle at `(msg, w)`, so aborted +attempts leave their challenge in the cache exactly as in the real experiment. -/ +noncomputable def realSignBody (pk : Stmt) (sk : Wit) (msg : M) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Option (Commit × Resp)) := + simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg maxAttempts) + +/-- One signing attempt of the all-attempts-reprogramming hybrid: commit honestly, then +overwrite the cache at `(msg, w)` with a fresh uniform challenge before responding. -/ +noncomputable def progSignAttempt (pk : Stmt) (sk : Wit) (msg : M) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Commit × Option Resp) := do + let (w, st) ← liftM (ids.commit pk sk) + let c ← (liftM (uniformSample Chal) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp Chal) + modify fun cache => cache.cacheQuery (msg, w) c + let oz ← liftM (ids.respond pk sk st c) + pure (w, oz) + +/-- Signing-oracle body of the all-attempts-reprogramming hybrid (Prog): run the restart +loop with `progSignAttempt`, so every attempt (accepted or rejected) reprograms the +random-oracle cache with a fresh challenge. -/ +noncomputable def progSignBody (pk : Stmt) (sk : Wit) (msg : M) : + ℕ → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Option (Commit × Resp)) + | 0 => pure none + | n + 1 => do + let (w, oz) ← progSignAttempt ids M pk sk msg + match oz with + | some z => pure (some (w, z)) + | none => progSignBody pk sk msg n + +/-- Shared cache-programming continuation of `transSignBody` and `simSignBody`: program +the accepted transcript's challenge into the cache at `(msg, w)` and return the +signature `(w, z)`; an all-abort loop outcome produces no signature and no programming. + +The continuation is a deterministic function of the loop outcome, so the gap between +the two hybrids reduces entirely to the gap between their private loops (see +`tvDist_run_transSignBody_simSignBody_le`). -/ +noncomputable def signProgramCont (msg : M) : + Option (Commit × Chal × Resp) → + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Option (Commit × Resp)) + | some (w, c, z) => do + modify fun cache => cache.cacheQuery (msg, w) c + pure (some (w, z)) + | none => pure none + +/-- Signing-oracle body of the accepted-only-reprogramming hybrid (Trans): the restart +loop runs privately on honest executions (`ids.honestExecution`, which samples its own +uniform challenge and never touches the cache); only the accepted transcript is +programmed into the cache. Rejected attempts leave no trace. -/ +noncomputable def transSignBody (pk : Stmt) (sk : Wit) (msg : M) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Option (Commit × Resp)) := + liftM (firstSome (ids.honestExecution pk sk) maxAttempts) >>= signProgramCont M msg + +/-- Signing-oracle body of the simulated hybrid (Sim): as `transSignBody`, with the +per-attempt HVZK simulator replacing the honest execution. The secret key is unused, so +this body can be run by the NMA reduction. -/ +noncomputable def simSignBody (pk : Stmt) (_sk : Wit) (msg : M) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp (Option (Commit × Resp)) := + liftM (firstSome (sim pk) maxAttempts) >>= signProgramCont M msg + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/BodyBounds.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/BodyBounds.lean new file mode 100644 index 000000000..51c41eae3 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/BodyBounds.lean @@ -0,0 +1,1403 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Projections + +/-! +# Ghost-layer machinery for Fiat-Shamir with aborts: BodyBounds + +Real-versus-reprogrammed signing-body bounds: the within-signing-query +collision budget, per-attempt collision and abort bounds, the deferred-sampling +(lazy) ghost read step and handler, the two body-level cores of the Sign → Prog +hop, and the per-target ghost-membership charge route. + +Part of the hybrid signing-body development for the CMA-to-NMA reduction; +`VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies` re-exports all of +its modules and holds the overview docstring. +-/ + +@[expose] public section + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) + +/-! ## Real-versus-reprogrammed signing-body bounds + +Quantitative body-level core of the Sign → Prog hop +(`probOutput_hybridExpAtKey_real_le_prog`): run equations unfolding one attempt of the +real signing loop (`fsAbortSignLoop` under the caching random oracle) and of the +reprogramming loop (`progSignBody`), the per-attempt commitment-collision and abort +bounds, the within-signing-query total-variation induction +(`ofReal_tvDist_run_fsAbortSignLoop_progSignBody_le`), and the expected cache-growth +bound for the reprogramming loop (`tsum_probOutput_run_progSignBody_mul_enncard_le`). -/ + +/-- Expectation of a nonnegative functional under a `pure` computation. -/ +lemma tsum_probOutput_pure_mul {β : Type} (y : β) (f : β → ℝ≥0∞) : + ∑' z, Pr[= z | (pure y : ProbComp β)] * f z = f y := by + rw [tsum_eq_single y fun z hz => by + rw [probOutput_eq_zero_of_not_mem_support (by simp [hz]), zero_mul]] + rw [probOutput_pure_self, one_mul] + +/-- Tonelli-style rearrangement: the expectation of a nonnegative functional under a +bind is the outer expectation of the inner expectations. -/ +lemma tsum_probOutput_bind_mul {α β : Type} (oa : ProbComp α) + (g : α → ProbComp β) (f : β → ℝ≥0∞) : + ∑' z, Pr[= z | oa >>= g] * f z = + ∑' x, Pr[= x | oa] * ∑' z, Pr[= z | g x] * f z := by + simp_rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + rw [ENNReal.tsum_comm] + simp_rw [mul_assoc, ENNReal.tsum_mul_left] + +/-- Push a constant out of a pointwise expectation bound: a (sub)probability average of +`f ≤ C + g` is at most `C` plus the average of `g`. -/ +lemma tsum_probOutput_mul_le_add_of_le {α : Type} (oa : ProbComp α) + {f g : α → ℝ≥0∞} {C : ℝ≥0∞} (h : ∀ a, f a ≤ C + g a) : + ∑' a, Pr[= a | oa] * f a ≤ C + ∑' a, Pr[= a | oa] * g a := by + calc ∑' a, Pr[= a | oa] * f a + ≤ ∑' a, (Pr[= a | oa] * C + Pr[= a | oa] * g a) := + ENNReal.tsum_le_tsum fun a => by rw [← mul_add]; exact mul_le_mul_right (h a) _ + _ = (∑' a, Pr[= a | oa]) * C + ∑' a, Pr[= a | oa] * g a := by + rw [ENNReal.tsum_add, ENNReal.tsum_mul_right] + _ ≤ 1 * C + ∑' a, Pr[= a | oa] * g a := by + gcongr + exact tsum_probOutput_le_one + _ = C + ∑' a, Pr[= a | oa] * g a := by rw [one_mul] + +/-- `ENNReal` form of `tvDist_bind_left_le`: the lifted TV distance of a bind over a +shared base is at most the base-averaged lifted TV distance of the continuations. -/ +lemma ofReal_tvDist_bind_le_tsum {α β : Type} (oa : ProbComp α) (f g : α → ProbComp β) : + ENNReal.ofReal (tvDist (oa >>= f) (oa >>= g)) ≤ + ∑' x, Pr[= x | oa] * ENNReal.ofReal (tvDist (f x) (g x)) := by + refine le_trans (ENNReal.ofReal_le_ofReal (tvDist_bind_left_le oa f g)) ?_ + have h_sum_ne_top : (∑' x : α, Pr[= x | oa]) ≠ ⊤ := + ne_top_of_le_ne_top ENNReal.one_ne_top tsum_probOutput_le_one + have h_summable : Summable fun x : α => Pr[= x | oa].toReal * tvDist (f x) (g x) := + Summable.of_nonneg_of_le + (fun x => mul_nonneg ENNReal.toReal_nonneg (tvDist_nonneg _ _)) + (fun x => mul_le_of_le_one_right ENNReal.toReal_nonneg (tvDist_le_one _ _)) + (ENNReal.summable_toReal h_sum_ne_top) + rw [ENNReal.ofReal_tsum_of_nonneg + (fun x => mul_nonneg ENNReal.toReal_nonneg (tvDist_nonneg _ _)) h_summable] + refine ENNReal.tsum_le_tsum fun x => ?_ + rw [ENNReal.ofReal_mul ENNReal.toReal_nonneg, ENNReal.ofReal_toReal probOutput_ne_top] + +omit [SampleableType Stmt] in +/-- One-attempt unfolding of the reprogramming loop's cache-level run. -/ +lemma run_progSignBody_succ (pk : Stmt) (sk : Wit) (msg : M) (n : ℕ) + (c : (M × Commit →ₒ Chal).QueryCache) : + (progSignBody ids M pk sk msg (n + 1)).run c = + ids.commit pk sk >>= fun ws => + uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => (progSignBody ids M pk sk msg n).run (c.cacheQuery (msg, ws.1) ch) := by + simp only [progSignBody, progSignAttempt, bind_assoc, StateT.run_bind, + OracleComp.liftM_run_StateT, pure_bind, StateT.run_modify] + refine congrArg (ids.commit pk sk >>= ·) (funext fun ws => ?_) + obtain ⟨w, st⟩ := ws + refine congrArg (uniformSample Chal >>= ·) (funext fun ch => ?_) + refine congrArg (ids.respond pk sk st ch >>= ·) (funext fun oz => ?_) + cases oz with + | some z => rfl + | none => rfl + +omit [SampleableType Stmt] in +/-- One-attempt unfolding of the real signing loop's cache-level run under the caching +random oracle: commit, take one `roStep` at the commitment point, respond against the +returned challenge, and either return or recurse on the post-step cache. -/ +lemma run_simulateQ_fsAbortSignLoop_succ (pk : Stmt) (sk : Wit) (msg : M) (n : ℕ) + (c : (M × Commit →ₒ Chal).QueryCache) : + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg (n + 1))).run c = + ids.commit pk sk >>= fun ws => + roStep M c (msg, ws.1) >>= fun chc => + ids.respond pk sk ws.2 chc.1 >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), chc.2) + | none => + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg n)).run chc.2 := by + simp only [fsAbortSignLoop, fsAbortSignAttempt, simulateQ_bind, + roSim.simulateQ_HasQuery_query, bind_assoc, StateT.run_bind] + rw [roSim.run_liftM, bind_map_left] + refine congrArg (ids.commit pk sk >>= ·) (funext fun ws => ?_) + obtain ⟨w, st⟩ := ws + rw [randomOracle_run_eq_roStep] + refine congrArg (roStep M c (msg, w) >>= ·) (funext fun chc => ?_) + rw [roSim.run_liftM, bind_map_left] + refine congrArg (ids.respond pk sk st chc.1 >>= ·) (funext fun oz => ?_) + cases oz with + | some z => rfl + | none => rfl + +/-! ### The within-signing-query collision budget -/ + +/-- Collision budget of one signing query of the Sign → Prog hop, as a function of the +attempt budget `n` and the starting cache size `N`: attempt `a` is reached with +probability at most `p ^ a` and collides with a cached point with probability at most +`(N + a) · ε`. -/ +noncomputable def signCollisionBound (ε p : ℝ) (n : ℕ) (N : ℝ≥0∞) : ℝ≥0∞ := + ENNReal.ofReal ε * ∑ a ∈ Finset.range n, ENNReal.ofReal p ^ a * (N + a) + +@[simp] +lemma signCollisionBound_zero (ε p : ℝ) (N : ℝ≥0∞) : + signCollisionBound ε p 0 N = 0 := by + simp [signCollisionBound] + +lemma signCollisionBound_succ (ε p : ℝ) (n : ℕ) (N : ℝ≥0∞) : + signCollisionBound ε p (n + 1) N = + N * ENNReal.ofReal ε + + ENNReal.ofReal p * signCollisionBound ε p n (N + 1) := by + have h : ∑ a ∈ Finset.range n, ENNReal.ofReal p ^ (a + 1) * (N + ↑(a + 1)) = + ENNReal.ofReal p * ∑ a ∈ Finset.range n, ENNReal.ofReal p ^ a * (N + 1 + ↑a) := by + rw [Finset.mul_sum] + refine Finset.sum_congr rfl fun a _ => ?_ + push_cast + ring + rw [signCollisionBound, signCollisionBound, Finset.sum_range_succ', h] + push_cast + ring + +lemma signCollisionBound_mono (ε p : ℝ) (n : ℕ) {N N' : ℝ≥0∞} (h : N ≤ N') : + signCollisionBound ε p n N ≤ signCollisionBound ε p n N' := by + unfold signCollisionBound + gcongr + +/-- Splitting of the collision budget into a state-free part and a part linear in the +starting cache size, matching the `ζ + R s · β` query-slack shape of +`OracleComp.ProgramLogic.Relational.expectedQuerySlack_expected_resource_le`. -/ +lemma signCollisionBound_eq (ε p : ℝ) (n : ℕ) (N : ℝ≥0∞) : + signCollisionBound ε p n N = + ENNReal.ofReal ε * ∑ a ∈ Finset.range n, (a : ℝ≥0∞) * ENNReal.ofReal p ^ a + + N * (ENNReal.ofReal ε * ∑ a ∈ Finset.range n, ENNReal.ofReal p ^ a) := by + rw [signCollisionBound] + simp only [Finset.mul_sum] + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun a _ => by ring + +/-! ### Per-attempt collision and abort bounds -/ + +omit [SampleableType Stmt] [DecidableEq Commit] in +/-- Aggregate per-attempt abort bound: the commit-averaged probability that a fresh +uniform challenge is refused equals the abort probability of one honest execution. -/ +lemma tsum_probOutput_commit_mul_abort_le (pk : Stmt) (sk : Wit) {p_abort : ℝ} + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] + ≤ ENNReal.ofReal p_abort := by + classical + refine le_trans (le_of_eq ?_) hAbort + rw [IdenSchemeWithAbort.honestExecution, probOutput_bind_eq_tsum] + refine tsum_congr fun ws => ?_ + obtain ⟨cm, st⟩ := ws + congr 1 + rw [probOutput_bind_eq_tsum, probOutput_bind_eq_tsum] + refine tsum_congr fun ch => ?_ + congr 1 + rw [probOutput_bind_eq_tsum, tsum_eq_single (none : Option Resp) ?_] + · simp [probOutput_pure] + · rintro (_ | z) hb + · exact absurd rfl hb + · simp [probOutput_pure] + +omit [SampleableType Stmt] [SampleableType Chal] [DecidableEq M] [DecidableEq Commit] in +/-- Commitment-guessing bound for cache hits: under a pointwise commitment-guessing +bound `ε`, one commit lands on a cached point of `c` at message `msg` with probability +at most `enncard c · ε`. -/ +lemma probEvent_commit_hit_le (pk : Stmt) (sk : Wit) {ε : ℝ} + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (msg : M) (c : (M × Commit →ₒ Chal).QueryCache) : + Pr[fun ws : Commit × PrvState => c (msg, ws.1) ≠ none | ids.commit pk sk] + ≤ QueryCache.enncard c * ENNReal.ofReal ε := by + classical + haveI : DecidableEq Commit := Classical.decEq Commit + let commitDist : ProbComp Commit := Prod.fst <$> ids.commit pk sk + let hit : Commit → Prop := fun w => c (msg, w) ≠ none + let S : Finset Commit := (finSupport commitDist).filter hit + have h_event : + Pr[fun ws : Commit × PrvState => c (msg, ws.1) ≠ none | ids.commit pk sk] + = Pr[hit | commitDist] := by + simp [commitDist, hit] + have h_sum : Pr[hit | commitDist] = ∑ w ∈ S, Pr[= w | commitDist] := by + simp [S, probEvent_eq_sum_filter_finSupport] + have h_sum_le : ∑ w ∈ S, Pr[= w | commitDist] ≤ ∑ w ∈ S, ENNReal.ofReal ε := + Finset.sum_le_sum fun w _ => hGuess w + have h_card_le : (S.card : ℝ≥0∞) ≤ QueryCache.enncard c := by + have hex : ∀ w : ↑(S : Set Commit), ∃ v : Chal, c (msg, w.1) = some v := fun w => + Option.ne_none_iff_exists'.mp ((Finset.mem_filter.mp w.2).2) + let cacheEntryOfHit : ↑(S : Set Commit) → c.toSet := fun w => + ⟨⟨(msg, w.1), Classical.choose (hex w)⟩, Classical.choose_spec (hex w)⟩ + have h_inj : Function.Injective cacheEntryOfHit := by + intro w₁ w₂ h + apply Subtype.ext + have hdomain : ((msg, w₁.1) : M × Commit) = (msg, w₂.1) := + congrArg (fun x : c.toSet => x.1.1) h + exact congrArg Prod.snd hdomain + have henc : (S : Set Commit).encard ≤ c.toSet.encard := by + simpa using Function.Embedding.encard_le ⟨cacheEntryOfHit, h_inj⟩ + have henc_nat : (S.card : ℕ∞) ≤ c.toSet.encard := by simpa using henc + exact ENat.toENNReal_mono henc_nat + calc Pr[fun ws : Commit × PrvState => c (msg, ws.1) ≠ none | ids.commit pk sk] + = Pr[hit | commitDist] := h_event + _ = ∑ w ∈ S, Pr[= w | commitDist] := h_sum + _ ≤ ∑ w ∈ S, ENNReal.ofReal ε := h_sum_le + _ = (S.card : ℝ≥0∞) * ENNReal.ofReal ε := by simp [Finset.sum_const, nsmul_eq_mul] + _ ≤ QueryCache.enncard c * ENNReal.ofReal ε := mul_le_mul' h_card_le le_rfl + +/-! ### Deferred-sampling read step (lazy ghost firing) + +The eager ghost handler `ghostHybridImpl … true` pre-populates the ghost cache during +signing and reads it deterministically, so an adversarial read at a ghost point flips the +bad flag with mass `1`. That deterministic flip is *not* amortized by `enncard · ε`, which +is why the charged-step premise of `probEvent_bad_simulateQ_run_le_expectedQuerySlack` +fails for the eager run. + +The fix is deferred sampling: postpone each rejected attempt's commitment draw to read +time. A read at point `(msg, w')` then redraws the `pending` deferred commitments and fires +iff one of them equals `w'`. Under the pointwise guessing bound `hGuess`, each redraw lands +on `w'` with probability `≤ ε`, so the union bound over `pending` redraws gives the per-read +charge `pending · ε` — exactly the `R s · ε` shape the accumulator's charged-step premise +demands, with `R s := enncard (ghost cache) = pending`. `lazyGhostFire` is that read step +and `probOutput_lazyGhostFire_true_le` is its charge bound. -/ + +/-- Deferred-sampling ghost read: draw `pending` fresh commitments and fire iff some draw +equals the adversary's read point `w'`. The lazy counterpart of the eager ghost-domain +membership test in `ghostHybridImpl … true`, with the rejected attempts' commitment draws +postponed to read time. -/ +noncomputable def lazyGhostFire (pk : Stmt) (sk : Wit) (w' : Commit) : + ℕ → ProbComp Bool + | 0 => pure false + | n + 1 => do + let w ← Prod.fst <$> ids.commit pk sk + let b ← lazyGhostFire pk sk w' n + pure (decide (w = w') || b) + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Single-pending deferred-sampling read** (draw-commutation, term form). With +exactly one pending ghost attempt the lazy read `lazyGhostFire … 1` is the deferred draw +`do w ← ids.commit pk sk; pure (decide (w = w'))`: a fresh commitment is sampled at read +time and compared against the adversary's read point `w'`. This is the term-level +draw-commutation that postpones a single rejected attempt's commitment draw to read time; +the eager handler instead reads the *already-sampled* `w` from the ghost cache. -/ +lemma lazyGhostFire_one_eq (pk : Stmt) (sk : Wit) (w' : Commit) : + lazyGhostFire ids pk sk w' 1 = + ((Prod.fst <$> ids.commit pk sk) >>= fun w => pure (decide (w = w'))) := by + change (Prod.fst <$> ids.commit pk sk >>= fun w => + lazyGhostFire ids pk sk w' 0 >>= fun b => pure (decide (w = w') || b)) = _ + simp only [lazyGhostFire, pure_bind, Bool.or_false] + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Single-pending lazy fire marginal** (draw-commutation, probability form). The +deferred read fires with probability exactly the commitment law's mass at the read point: +`Pr[fire | lazyGhostFire … 1] = Pr[= w' | Prod.fst <$> ids.commit pk sk]`. This is the +read-time marginal that the body-level deferred-sampling commutation must match against the +eager handler's signing-time draw of the same commitment: the eager run draws `w` while +signing and fires deterministically on a structural ghost hit `w = w'`, whose marginal over +that earlier draw is the *same* `Pr[= w']`. The single-pending case of the Fubini/tsum-swap +that moves the sampling site from signing time to read time. -/ +lemma probOutput_lazyGhostFire_one (pk : Stmt) (sk : Wit) (w' : Commit) : + Pr[= true | lazyGhostFire ids pk sk w' 1] = Pr[= w' | Prod.fst <$> ids.commit pk sk] := by + rw [lazyGhostFire_one_eq ids pk sk w', probOutput_bind_eq_tsum] + rw [tsum_eq_single w' ?_] + · simp [probOutput_pure] + · intro b hb + simp [probOutput_pure, hb] + +omit [SampleableType Stmt] in +/-- **Eager read bad-fire indicator.** Starting from a state with the bad flag unset, the +eager ghost handler's adversarial random-oracle read at `mc` sets the bad flag with mass +exactly `1` if `mc` lies in the ghost-cache domain and mass `0` otherwise: on a ghost hit +the handler returns `pure (v, (s.1, true))`, and on a ghost miss it runs `roStep` which +leaves the (already-unset) bad flag untouched. This is the deterministic eager flip whose +upstream-averaged marginal the deferred-sampling commutation must reproduce. -/ +lemma probOutput_ghostHybridImpl_read_bad + (pk : Stmt) (sk : Wit) (mc : M × Commit) + (s : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) : + Pr[fun z : Chal × GhostState M Commit Chal => z.2.2 = true | + (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run (s, false)] = + if s.1.2 mc = none then 0 else 1 := by + simp only [ghostHybridImpl, StateT.run_mk] + cases hgh : s.1.2 mc with + | some v => + simp only [↓reduceIte] + simp + | none => + simp [probEvent_eq_zero] + +omit [SampleableType Stmt] in +/-- **Single-pending deferred-sampling commutation** (the base case of the deferred-sampling +induction; the multi-pending iteration is `probOutput_eagerMultiReadBad_eq_lazyFire_or`). + +The eager handler reads the *already-sampled* ghost key `w` from the ghost cache and fires +the bad flag deterministically iff the adversary's read point `mc` matches the written entry +`(msg, w)`. Marginalizing over the single signing-time commit draw of `w` (which wrote +`(msg, w)` into an initially-empty ghost layer over an arbitrary real layer `re` at challenge +`c`), the eager bad-fire mass equals the deferred read `lazyGhostFire … 1`, in which the same +commitment is *redrawn at read time*. Concretely both equal `Pr[= mc.2 | commit]` when the +read point lies under `msg` (and `0` otherwise): the eager structural hit `w = mc.2` over the +signing-time draw and the lazy fresh draw `w = mc.2` at read time have the *same* marginal. + +This is the formal statement that moves the sampling site from signing time (eager) to read +time (lazy) for a single pending key. The averaging over the upstream draw — not the +per-state eager value — is what reproduces the lazy mass: at a *fixed* drawn `w` the eager +read is deterministic `0`/`1`, but its expectation over `w ← commit` is exactly the lazy +`lazyGhostFire … 1` firing probability. The general leaf iterates this peel over the random +number of pending keys (`probOutput_eagerMultiReadBad_eq_lazyFire_or`). -/ +lemma probEvent_ghostHybridImpl_read_bad_single_eq_lazyFire + (pk : Stmt) (sk : Wit) (msg : M) (mc : M × Commit) (hmc : mc.1 = msg) + (re : (M × Commit →ₒ Chal).QueryCache) (c : Chal) : + Pr[fun z : Chal × GhostState M Commit Chal => z.2.2 = true | + (Prod.fst <$> ids.commit pk sk) >>= fun w => + (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run + (((re, (∅ : (M × Commit →ₒ Chal).QueryCache).cacheQuery (msg, w) c), []), false)] = + Pr[= true | lazyGhostFire ids pk sk mc.2 1] := by + -- Reduce the eager single-write read to its bad-fire indicator, which collapses to + -- `if w = mc.2 then 1 else 0` (membership of `mc` in the single-entry ghost cache). + have hind : ∀ w : Commit, + (if ((((re, (∅ : (M × Commit →ₒ Chal).QueryCache).cacheQuery (msg, w) c), []) + : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M).1.2 + mc = none) then (0 : ℝ≥0∞) else 1) = if w = mc.2 then 1 else 0 := by + intro w + by_cases hw : w = mc.2 + · subst hw + have hmceq : (mc : M × Commit) = (msg, mc.2) := Prod.ext hmc rfl + conv_lhs => rw [show ((mc : M × Commit)) = (msg, mc.2) from hmceq] + simp + · have hne : (mc : M × Commit) ≠ (msg, w) := by + intro h; exact hw (by rw [← hmc] at h; exact (Prod.ext_iff.mp h).2.symm) + simp only [QueryCache.cacheQuery_of_ne _ _ hne] + simp [hw] + rw [probEvent_bind_eq_tsum] + simp only [probOutput_ghostHybridImpl_read_bad ids M maxAttempts pk sk mc, hind] + rw [probOutput_lazyGhostFire_one] + rw [tsum_eq_single mc.2 (by intro w hw; simp [hw])] + simp + +omit [SampleableType Stmt] in +/-- **Eager multi-pending ghost read marginal** (the deferred-sampling iteration over the +number of pending ghost keys). + +The eager read at a ghost cache holding the entries `(msg, w_i)` for `n` signing-time-drawn +keys fires the bad flag iff the adversary's read point `mc` matches one of those `n` entries. +Marginalizing over the `n` signing-time draws (each fresh `w ← Prod.fst <$> ids.commit`, the +ghost cache built over an arbitrary real layer `re` at challenges `c`), the eager bad-fire +mass equals the deferred read `lazyGhostFire … n`, in which all `n` commitments are *redrawn at +read time*. + +This is the iteration of the single-pending commutation +`probEvent_ghostHybridImpl_read_bad_single_eq_lazyFire` over the number of pending keys: +the membership event `mc ∈ {(msg, w_i)}` is the union `∃ i, w_i = mc.2`, whose marginal over +iid draws is exactly the union event of `lazyGhostFire`. The induction peels one draw — the +freshly written entry decides one disjunct (`decide (w = mc.2)`), the remaining `n` entries +recurse — matching `lazyGhostFire`'s `decide (w = w') || b` step verbatim. + +`eagerMultiReadBad re n` is the eager bad-fire flag at an `n`-pending cache, written as the +deferred draw whose `decide`-or fold tracks the membership; each draw writes `(msg, w)` over +`re` and the read is the deterministic eager indicator +`probOutput_ghostHybridImpl_read_bad`. -/ +noncomputable def eagerMultiReadBad (pk : Stmt) (sk : Wit) (msg : M) (mc : M × Commit) + (re : (M × Commit →ₒ Chal).QueryCache) (c : Chal) : + ℕ → ProbComp Bool + | 0 => pure (decide (re mc ≠ none)) + | n + 1 => do + let w ← Prod.fst <$> ids.commit pk sk + eagerMultiReadBad pk sk msg mc (re.cacheQuery (msg, w) c) c n + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Eager multi-pending read = lazy fire (union form)** (the iteration core over the +pending count). + +By induction on the pending count `n`, the eager `n`-write membership read over an arbitrary +base layer `re` equals the deferred `lazyGhostFire … n` *or-ed* with the base-layer membership +`re mc ≠ none`. Each induction step peels one signing-time draw `w ← commit`: the freshly +written entry `(msg, w)` contributes the disjunct `decide (w = mc.2)` (since `mc.1 = msg`, +membership of `mc` in the one-key extension `re.cacheQuery (msg, w) c` is `w = mc.2 ∨ mc ∈ re`), +matching `lazyGhostFire`'s `decide (w = w') || b` step verbatim. This is the deferred-sampling +commutation iterated over all pending keys — moving every signing-time draw to read time — +proved entirely locally (no adversary fold). -/ +lemma probOutput_eagerMultiReadBad_eq_lazyFire_or + (pk : Stmt) (sk : Wit) (msg : M) (mc : M × Commit) (hmc : mc.1 = msg) (c : Chal) : + ∀ (n : ℕ) (re : (M × Commit →ₒ Chal).QueryCache), + Pr[= true | eagerMultiReadBad ids M pk sk msg mc re c n] = + Pr[= true | lazyGhostFire ids pk sk mc.2 n >>= fun b => + pure (b || decide (re mc ≠ none))] := by + intro n + induction n with + | zero => + intro re + simp [eagerMultiReadBad, lazyGhostFire] + | succ n ih => + intro re + -- Peel one signing-time draw on both sides and rewrite the membership disjunct. + have hcache : ∀ w : Commit, + decide ((re.cacheQuery (msg, w) c) mc ≠ none) = + (decide (w = mc.2) || decide (re mc ≠ none)) := by + intro w + by_cases hw : w = mc.2 + · subst hw + have hmceq : (mc : M × Commit) = (msg, mc.2) := Prod.ext hmc rfl + rw [hmceq, QueryCache.cacheQuery_self] + simp + · have hne : (mc : M × Commit) ≠ (msg, w) := by + intro h; exact hw (by rw [← hmc] at h; exact (Prod.ext_iff.mp h).2.symm) + rw [QueryCache.cacheQuery_of_ne _ _ hne] + simp [hw] + -- LHS: `eagerMultiReadBad (n+1)` draws `w` then recurses; apply `ih` on the extended cache. + rw [show eagerMultiReadBad ids M pk sk msg mc re c (n + 1) = + (Prod.fst <$> ids.commit pk sk) >>= fun w => + eagerMultiReadBad ids M pk sk msg mc (re.cacheQuery (msg, w) c) c n + from rfl] + rw [probOutput_bind_eq_tsum] + -- RHS: `lazyGhostFire (n+1)` draws `w`, then `lazyGhostFire n` and or-s `decide (w = mc.2)`. + rw [show (lazyGhostFire ids pk sk mc.2 (n + 1) >>= fun b => pure (b || decide (re mc ≠ none))) + = (Prod.fst <$> ids.commit pk sk) >>= fun w => + (lazyGhostFire ids pk sk mc.2 n >>= fun b => + pure (b || decide ((re.cacheQuery (msg, w) c) mc ≠ none))) + from ?_] + · rw [probOutput_bind_eq_tsum] + refine tsum_congr fun w => ?_ + rw [ih (re.cacheQuery (msg, w) c)] + · -- The two read-time draw shapes agree by `hcache` and Boolean-or associativity. + rw [show (lazyGhostFire ids pk sk mc.2 (n + 1)) = + (Prod.fst <$> ids.commit pk sk) >>= fun w => + lazyGhostFire ids pk sk mc.2 n >>= fun b => pure (decide (w = mc.2) || b) + from rfl] + rw [bind_assoc] + refine bind_congr fun w => ?_ + rw [bind_assoc] + refine bind_congr fun b => ?_ + rw [pure_bind, hcache w] + congr 1 + cases b <;> cases (decide (w = mc.2)) <;> cases (decide (re mc ≠ none)) <;> rfl + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Eager multi-pending read = lazy fire** (empty base case). With an *empty* base layer +(`re = ∅`, the actual initial real cache of the leaf), the eager `n`-write membership read has +exactly the firing probability of the deferred read `lazyGhostFire … n`: the union over the +`n` signing-time draws equals the union over the `n` read-time redraws. -/ +lemma probOutput_eagerMultiReadBad_empty_eq_lazyFire + (pk : Stmt) (sk : Wit) (msg : M) (mc : M × Commit) (hmc : mc.1 = msg) (c : Chal) (n : ℕ) : + Pr[= true | eagerMultiReadBad ids M pk sk msg mc + (∅ : (M × Commit →ₒ Chal).QueryCache) c n] = + Pr[= true | lazyGhostFire ids pk sk mc.2 n] := by + rw [probOutput_eagerMultiReadBad_eq_lazyFire_or ids M pk sk msg mc hmc c n] + congr 1 + rw [show (fun b => pure (b || decide ((∅ : (M × Commit →ₒ Chal).QueryCache) mc ≠ none))) + = (fun b : Bool => pure b) from funext fun b => by simp] + rw [bind_pure] + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] in +/-- Boolean-or read shape: appending one fresh `decide (w = w')` flag to a Boolean draw +raises the firing probability by at most `1` (when the fresh flag is set) over the residual +draw. The per-summand step of `probOutput_lazyGhostFire_true_le`. -/ +lemma probOutput_bind_or_pure_le (q : Bool) (mb : ProbComp Bool) : + Pr[= true | mb >>= fun b => pure (q || b)] ≤ (if q then 1 else 0) + Pr[= true | mb] := by + cases q with + | true => simp + | false => simp + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Charged-step bound for the lazy ghost read.** Under the pointwise commitment-guessing +bound `ε`, the deferred-sampling read `lazyGhostFire … pending` fires with probability at +most `pending · ε`. This is the `R s · ε` charge that makes the charged-step premise of +`probEvent_bad_simulateQ_run_le_expectedQuerySlack` *true* for the lazy run (with +`R s := enncard (ghost cache) = pending`), in contrast to the eager run's deterministic +flip. Proved by a union bound over the `pending` redraws, each bounded by `hGuess`. -/ +lemma probOutput_lazyGhostFire_true_le (pk : Stmt) (sk : Wit) {ε : ℝ} + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (w' : Commit) : + ∀ n : ℕ, Pr[= true | lazyGhostFire ids pk sk w' n] ≤ (n : ℝ≥0∞) * ENNReal.ofReal ε := by + intro n + induction n with + | zero => simp [lazyGhostFire] + | succ n ih => + have hbody : Pr[= true | lazyGhostFire ids pk sk w' (n + 1)] ≤ + Pr[= w' | Prod.fst <$> ids.commit pk sk] + + Pr[= true | lazyGhostFire ids pk sk w' n] := by + change Pr[= true | (Prod.fst <$> ids.commit pk sk) >>= fun w => + lazyGhostFire ids pk sk w' n >>= fun b => pure (decide (w = w') || b)] ≤ _ + rw [probOutput_bind_eq_tsum] + calc (∑' w : Commit, Pr[= w | Prod.fst <$> ids.commit pk sk] * + Pr[= true | lazyGhostFire ids pk sk w' n >>= + fun b => pure (decide (w = w') || b)]) + ≤ ∑' w : Commit, Pr[= w | Prod.fst <$> ids.commit pk sk] * + ((if w = w' then 1 else 0) + + Pr[= true | lazyGhostFire ids pk sk w' n]) := by + refine ENNReal.tsum_le_tsum fun w => ?_ + gcongr + have h := probOutput_bind_or_pure_le (decide (w = w')) + (lazyGhostFire ids pk sk w' n) + simp only [decide_eq_true_eq] at h + exact h + _ = (∑' w : Commit, Pr[= w | Prod.fst <$> ids.commit pk sk] * + (if w = w' then 1 else 0)) + + (∑' w : Commit, Pr[= w | Prod.fst <$> ids.commit pk sk]) * + Pr[= true | lazyGhostFire ids pk sk w' n] := by + rw [← ENNReal.tsum_mul_right, ← ENNReal.tsum_add] + exact tsum_congr fun w => by ring + _ ≤ Pr[= w' | Prod.fst <$> ids.commit pk sk] + + Pr[= true | lazyGhostFire ids pk sk w' n] := by + gcongr + · rw [tsum_eq_single w' (by intro b hb; simp [hb]), if_pos rfl, mul_one] + · exact mul_le_of_le_one_left (zero_le) tsum_probOutput_le_one + refine hbody.trans ?_ + push_cast + rw [add_mul, one_mul, add_comm] + gcongr + exact hGuess w' + +omit [SampleableType Stmt] [SampleableType Chal] [DecidableEq M] in +/-- The lazy ghost read fires with probability at most `enncard gh · ε`, where the deferred +attempt count is the ghost cache size. The `R s · ε` charge in the shape consumed by the +accumulator's charged-step premise (`R s := QueryCache.enncard`). -/ +lemma probOutput_lazyGhostFire_true_le_enncard (pk : Stmt) (sk : Wit) {ε : ℝ} + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (w' : Commit) (gh : (M × Commit →ₒ Chal).QueryCache) + (hpending : (gh.toSet.encard.toNat : ℝ≥0∞) ≤ QueryCache.enncard gh) : + Pr[= true | lazyGhostFire ids pk sk w' gh.toSet.encard.toNat] + ≤ QueryCache.enncard gh * ENNReal.ofReal ε := by + refine (probOutput_lazyGhostFire_true_le ids pk sk hGuess w' _).trans ?_ + gcongr + +/-! ### Deferred-sampling (lazy) ghost-instrumented hybrid handler + +`lazyGhostHybridImpl` is the deferred-sampling counterpart of `ghostHybridImpl … true`. +It carries the *same* layered-cache-plus-flag state `GhostState`, and signs with the same +`ghostSignBody` (so the ghost layer records the same per-attempt programmings and grows by +the same amount). The only change is the adversarial random-oracle read step: instead of +the eager deterministic ghost lookup that flips the bad flag with mass `1`, the read draws +`lazyGhostFire` over the *pending count* `enncard (ghost cache)` and fires the bad flag with +probability `≤ enncard (ghost cache) · ε` (the deferred-sampling charge +`probOutput_lazyGhostFire_true_le_enncard`). The answer to the adversary is taken from the +real layer via `roStep`, independently of the fire draw. This is the handler for which the +charged-step premise of `probEvent_bad_simulateQ_run_le_expectedQuerySlack` holds. -/ + +/-- Deferred-sampling ghost-instrumented hybrid handler: signs with `ghostSignBody`, answers +uniform queries by forwarding, and answers adversarial random-oracle reads from the real +layer while firing the bad flag lazily (`lazyGhostFire` over the pending ghost count). -/ +noncomputable def lazyGhostHybridImpl (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (GhostState M Commit Chal) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + lazyGhostFire ids pk sk mc.2 s.1.1.2.toSet.encard.toNat >>= fun fired => + (fun cu => (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || fired))) <$> roStep M s.1.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc => (alc.1, ((alc.2, msg :: s.1.2), s.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run s.1.1 + +omit [SampleableType Stmt] in +/-- The eager (`ghostHybridImpl … true`) and lazy (`lazyGhostHybridImpl`) ghost handlers +are *definitionally identical* on uniform queries: both forward the query and leave the +state untouched. The two handlers differ only in the adversarial random-oracle read step +(`.inl (.inr _)`), where the eager handler reads the pre-populated ghost cache and flips +the bad flag deterministically while the lazy handler answers from the real layer and +fires the flag via the deferred-sampling draw `lazyGhostFire`. -/ +lemma lazyGhostHybridImpl_run_unif_eq (pk : Stmt) (sk : Wit) (n : unifSpec.Domain) + (s : GhostState M Commit Chal) : + (lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inl n))).run s = + (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inl n))).run s := rfl + +omit [SampleableType Stmt] in +/-- The eager and lazy ghost handlers are *definitionally identical* on signing queries: +both run `ghostSignBody` on the cache layers, prepend `msg` to the signed-message list, and +leave the bad flag untouched. Together with `lazyGhostHybridImpl_run_unif_eq`, this isolates +the entire eager↔lazy distributional gap to the random-oracle read step. -/ +lemma lazyGhostHybridImpl_run_sign_eq (pk : Stmt) (sk : Wit) (msg : M) + (s : GhostState M Commit Chal) : + (lazyGhostHybridImpl ids M maxAttempts pk sk (.inr msg)).run s = + (ghostHybridImpl ids M maxAttempts true pk sk (.inr msg)).run s := rfl + +omit [SampleableType Stmt] in +/-- **Bad-flag absorption for the eager run.** Once the bad flag is set, the eager hybrid +run keeps it set: every output of `(simulateQ (ghostHybridImpl … true) oa).run p` from a +state `p` with `p.2 = true` again has its bad flag set. This lifts the per-step monotonicity +`ghostHybridImpl_bad_mono` through the whole free-monad fold. It is the support fact behind +the read-step HIT collapse: at a structural ghost hit the eager read forces the bad flag to +`true` and returns the *real cache untouched*, so the continuation run cannot lower the +charge — its bad mass is the run's success mass. -/ +lemma support_simulateQ_ghostHybridImpl_bad + (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) + (p : GhostState M Commit Chal) (hp : p.2 = true) : + ∀ z ∈ support ((simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) oa).run p), + z.2.2 = true := by + induction oa using OracleComp.inductionOn generalizing p with + | pure x => + intro z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz + exact hp + | query_bind t cont ih => + intro z hz + rw [simulateQ_bind, simulateQ_query, StateT.run_bind, support_bind] at hz + simp only [OracleQuery.input_query, OracleQuery.cont_query, Set.mem_iUnion] at hz + obtain ⟨y, hy, hz⟩ := hz + refine ih y.1 y.2 ?_ z hz + exact ghostHybridImpl_bad_mono ids M maxAttempts true pk sk t p hp y (by simpa using hy) + +omit [SampleableType Stmt] in +/-- **Eager bad mass from a set flag is the run's success mass.** Starting the eager run from +a state whose bad flag is already set, the probability the bad flag is set in the output +equals the (unconditional) probability the run produces *any* output — i.e. the success mass +`1 - probFailure`. Immediate from `support_simulateQ_ghostHybridImpl_bad` via +`probEvent_congr'`: on the support the event `z.2.2 = true` is constantly `True`. This is the +read-step HIT value: after the eager read forces the flag at the committed state `p` (with +the real cache untouched, `z = (v, (p.1, true))`), the continuation `cont v` contributes its +full success mass to the bad charge. -/ +lemma probEvent_simulateQ_ghostHybridImpl_bad_eq_true + (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) + (p : GhostState M Commit Chal) (hp : p.2 = true) : + Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) oa).run p] = + Pr[fun _ => True | (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) oa).run p] := by + refine probEvent_congr' (fun z hz => ?_) rfl + simp [support_simulateQ_ghostHybridImpl_bad ids M maxAttempts pk sk oa p hp z hz] + +omit [SampleableType Stmt] in +/-- **Eager read HIT charge.** At a state `p` whose ghost cache *hits* the adversary's read +point (`p.1.1.2 mc = some v`), the eager read flips the bad flag and returns the real cache +*untouched* as the single output `(v, (p.1, true))`. The contribution of this read to the +telescoped bad average therefore collapses to a single term: the continuation `cont v` run +from `(p.1, true)`, whose bad mass is its full success mass +(`probEvent_simulateQ_ghostHybridImpl_bad_eq_true`). This is the per-state HIT value the +read-step ∑-over-`p` collapse charges; averaging it over the upstream commit draws (the +pushforward law of `p`) is where the eager signing-time hit marginalizes to the lazy +read-time fire mass. -/ +lemma tsum_ghostHybridImpl_read_hit_eq + (pk : Stmt) (sk : Wit) (mc : M × Commit) + (cont : Chal → OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) + (p : GhostState M Commit Chal) (v : Chal) (hgh : p.1.1.2 mc = some v) : + (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2]) = + Pr[fun _ => True | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont v)).run (p.1, true)] := by + have hrun : (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p = + (pure (v, (p.1, true)) : + ProbComp (Chal × GhostState M Commit Chal)) := by + simp only [ghostHybridImpl, StateT.run_mk, hgh, if_pos trivial] + rfl + rw [hrun] + refine (tsum_probOutput_pure_mul (β := Chal × GhostState M Commit Chal) (v, (p.1, true)) + fun z => Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2]).trans ?_ + exact probEvent_simulateQ_ghostHybridImpl_bad_eq_true ids M maxAttempts pk sk (cont v) + (p.1, true) rfl + +omit [SampleableType Stmt] in +/-- **Per-state eager read-step inner-sum split.** The telescoped eager read contribution at +a fixed starting state `p` splits by the ghost-domain test `p.1.1.2 mc`: + +* on a **HIT** (`some v`) the eager read returns the ghost value with the real cache untouched + and forces the bad flag, so the inner `∑'z` collapses (by `tsum_ghostHybridImpl_read_hit_eq`) + to the continuation's full success mass from the flagged state `(p.1, true)`; +* on a **MISS** (`none`) the read runs `roStep` on the real layer leaving the bad flag + unchanged, so the inner `∑'z` is left untouched. + +This is the per-state structural decomposition that isolates the read-step's HIT charge — the +quantity whose `μ`-average against the upstream commit draws carries the deferred-sampling +content. It is the eager-side companion of the lazy read step (`lazyGhostHybridImpl` over the +pending ghost count) and is purely structural (no probabilistic content beyond the HIT +collapse `tsum_ghostHybridImpl_read_hit_eq`). -/ +lemma tsum_ghostHybridImpl_read_step_split + (pk : Stmt) (sk : Wit) (mc : M × Commit) + (cont : Chal → OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) + (p : GhostState M Commit Chal) : + (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2]) = + match p.1.1.2 mc with + | some v => Pr[fun _ => True | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont v)).run (p.1, true)] + | none => ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2] := by + cases h : p.1.1.2 mc with + | some v => rw [tsum_ghostHybridImpl_read_hit_eq ids M maxAttempts pk sk mc cont p v h] + | none => rfl + +/-! ### The two body-level cores of the Sign → Prog hop -/ + +omit [SampleableType Stmt] in +/-- **Within-signing-query TV induction for the Sign → Prog hop.** From a shared +starting cache, the real signing loop (live caching random oracle) and the +all-attempts-reprogramming loop are within total-variation distance +`ε · ∑_{a ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + ∀ (n : ℕ) (c : (M × Commit →ₒ Chal).QueryCache), + ENNReal.ofReal (tvDist + ((simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg n)).run c) + ((progSignBody ids M pk sk msg n).run c)) + ≤ signCollisionBound ε p_abort n (QueryCache.enncard c) := by + intro n + induction n with + | zero => + intro c + simp [fsAbortSignLoop, progSignBody] + | succ n ih => + intro c + classical + rw [run_simulateQ_fsAbortSignLoop_succ, run_progSignBody_succ] + refine le_trans (ofReal_tvDist_bind_le_tsum _ _ _) ?_ + set B' := signCollisionBound ε p_abort n (QueryCache.enncard c + 1) with hB' + have key : ∀ ws : Commit × PrvState, + ENNReal.ofReal (tvDist + (roStep M c (msg, ws.1) >>= fun chc => + ids.respond pk sk ws.2 chc.1 >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), chc.2) + | none => + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg n)).run chc.2) + (uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (progSignBody ids M pk sk msg n).run (c.cacheQuery (msg, ws.1) ch))) + ≤ (if c (msg, ws.1) = none then 0 else 1) + + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + B' := by + intro ws + cases hc : c (msg, ws.1) with + | some v => + rw [if_neg (by simp)] + exact le_add_right (ENNReal.ofReal_le_one.mpr (tvDist_le_one _ _)) + | none => + rw [if_pos rfl, zero_add, roStep_of_none M hc] + simp only [bind_assoc, pure_bind] + refine le_trans (ofReal_tvDist_bind_le_tsum _ _ _) ?_ + have hch : ∀ ch : Chal, + ENNReal.ofReal (tvDist + (ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (fsAbortSignLoop ids M pk sk msg n)).run + (c.cacheQuery (msg, ws.1) ch)) + (ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (progSignBody ids M pk sk msg n).run + (c.cacheQuery (msg, ws.1) ch))) + ≤ Pr[= none | ids.respond pk sk ws.2 ch] * B' := by + intro ch + refine le_trans (ofReal_tvDist_bind_le_tsum _ _ _) ?_ + calc ∑' oz : Option Resp, Pr[= oz | ids.respond pk sk ws.2 ch] * + ENNReal.ofReal (tvDist _ _) + ≤ ∑' oz : Option Resp, Pr[= oz | ids.respond pk sk ws.2 ch] * + (if oz = none then B' else 0) := by + refine ENNReal.tsum_le_tsum fun oz => mul_le_mul_right ?_ _ + cases oz with + | some z => simp + | none => + rw [if_pos rfl] + exact le_trans (ih (c.cacheQuery (msg, ws.1) ch)) + (signCollisionBound_mono ε p_abort n + (QueryCache.enncard_cacheQuery_le c (msg, ws.1) ch)) + _ = Pr[= none | ids.respond pk sk ws.2 ch] * B' := by + rw [tsum_eq_single (none : Option Resp) + fun oz hoz => by simp [hoz]] + simp + calc ∑' ch : Chal, Pr[= ch | uniformSample Chal] * + ENNReal.ofReal (tvDist _ _) + ≤ ∑' ch : Chal, Pr[= ch | uniformSample Chal] * + (Pr[= none | ids.respond pk sk ws.2 ch] * B') := + ENNReal.tsum_le_tsum fun ch => mul_le_mul_right (hch ch) _ + _ = Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * B' := by + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact tsum_congr fun ch => (mul_assoc _ _ _).symm + calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ENNReal.ofReal (tvDist _ _) + ≤ ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ((if c (msg, ws.1) = none then 0 else 1) + + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * B') := + ENNReal.tsum_le_tsum fun ws => mul_le_mul_right (key ws) _ + _ = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (if c (msg, ws.1) = none then 0 else 1)) + + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * B') := by + simp_rw [mul_add] + exact ENNReal.tsum_add + _ ≤ QueryCache.enncard c * ENNReal.ofReal ε + ENNReal.ofReal p_abort * B' := by + refine add_le_add ?_ ?_ + · refine le_trans (le_of_eq ?_) + (probEvent_commit_hit_le ids M pk sk hGuess msg c) + rw [probEvent_eq_tsum_ite] + refine tsum_congr fun ws => ?_ + by_cases h : c (msg, ws.1) = none <;> simp [h] + · calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * B') + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * B' := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * B' := + mul_le_mul_left + (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + _ = signCollisionBound ε p_abort (n + 1) (QueryCache.enncard c) := + (signCollisionBound_succ ε p_abort n (QueryCache.enncard c)).symm + +omit [SampleableType Stmt] in +/-- **Expected cache growth of the reprogramming loop.** Each attempt of `progSignBody` +programs at most one new cache point and the loop continues only on a fresh-challenge +rejection, so the expected size of the final cache is at most `|c| + ∑_{a + intro c + simp only [progSignBody, StateT.run_pure, tsum_probOutput_pure_mul] + simp + | succ n ih => + intro c + classical + set S : ℝ≥0∞ := ∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ a with hS + have hSucc : ∑ a ∈ Finset.range (n + 1), ENNReal.ofReal p_abort ^ a = + 1 + ENNReal.ofReal p_abort * S := by + rw [Finset.sum_range_succ', pow_zero, add_comm] + congr 1 + rw [Finset.mul_sum] + exact Finset.sum_congr rfl fun a _ => pow_succ' _ _ + rw [run_progSignBody_succ, tsum_probOutput_bind_mul] + have h_ws : ∀ ws : Commit × PrvState, + (∑' z : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (progSignBody ids M pk sk msg n).run (c.cacheQuery (msg, ws.1) ch)] * + QueryCache.enncard z.2) + ≤ (QueryCache.enncard c + 1) + + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + S := by + intro ws + rw [tsum_probOutput_bind_mul] + have h_ch : ∀ ch : Chal, + (∑' z : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (progSignBody ids M pk sk msg n).run + (c.cacheQuery (msg, ws.1) ch)] * + QueryCache.enncard z.2) + ≤ (QueryCache.enncard c + 1) + + Pr[= none | ids.respond pk sk ws.2 ch] * S := by + intro ch + rw [tsum_probOutput_bind_mul] + have h_oz : ∀ oz : Option Resp, + (∑' z : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (match oz with + | some z => pure (some (ws.1, z), c.cacheQuery (msg, ws.1) ch) + | none => + (progSignBody ids M pk sk msg n).run + (c.cacheQuery (msg, ws.1) ch) : + ProbComp (Option (Commit × Resp) × + (M × Commit →ₒ Chal).QueryCache))] * + QueryCache.enncard z.2) + ≤ (QueryCache.enncard c + 1) + (if oz = none then S else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (by simp), add_zero, tsum_probOutput_pure_mul] + exact QueryCache.enncard_cacheQuery_le c (msg, ws.1) ch + | none => + rw [if_pos rfl] + refine le_trans (ih (c.cacheQuery (msg, ws.1) ch)) ?_ + exact add_le_add_left + (QueryCache.enncard_cacheQuery_le c (msg, ws.1) ch) S + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_oz) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by simp [hoz]] + simp [mul_comm] + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ch) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact tsum_congr fun ch => (mul_assoc _ _ _).symm + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ws) ?_ + rw [hSucc] + have : ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + ≤ ENNReal.ofReal p_abort * S := by + calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * S := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * S := + mul_le_mul_left + (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + calc QueryCache.enncard c + 1 + + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + ≤ QueryCache.enncard c + 1 + ENNReal.ofReal p_abort * S := + add_le_add_right this _ + _ = QueryCache.enncard c + (1 + ENNReal.ofReal p_abort * S) := by + rw [add_assoc] + +omit [SampleableType Stmt] in +/-- One-attempt unfolding of the ghost reprogramming loop's layered-cache run. -/ +lemma run_ghostSignBody_succ (pk : Stmt) (sk : Wit) (msg : M) (n : ℕ) + (re gh : (M × Commit →ₒ Chal).QueryCache) : + (ghostSignBody ids M pk sk msg (n + 1)).run (re, gh) = + ids.commit pk sk >>= fun ws => + uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run (re, gh.cacheQuery (msg, ws.1) ch) := by + simp only [ghostSignBody, bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, + pure_bind] + refine congrArg (ids.commit pk sk >>= ·) (funext fun ws => ?_) + obtain ⟨w, st⟩ := ws + refine congrArg (uniformSample Chal >>= ·) (funext fun ch => ?_) + refine congrArg (ids.respond pk sk st ch >>= ·) (funext fun oz => ?_) + cases oz with + | some z => rfl + | none => rfl + +omit [SampleableType Stmt] in +/-- **Expected ghost-layer growth of the reprogramming loop.** Each rejected attempt of +`ghostSignBody` programs at most one new ghost-cache point; an accepted attempt only +*removes* a point from the ghost layer (`uncacheQuery`). The loop continues only on a +fresh-challenge rejection, so the expected size of the final ghost cache is at most +`|gh| + ∑_{a + intro re gh + simp only [ghostSignBody, StateT.run_pure, tsum_probOutput_pure_mul] + simp + | succ n ih => + intro re gh + classical + set S : ℝ≥0∞ := ∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ a with hS + have hSucc : ∑ a ∈ Finset.range (n + 1), ENNReal.ofReal p_abort ^ a = + 1 + ENNReal.ofReal p_abort * S := by + rw [Finset.sum_range_succ', pow_zero, add_comm] + congr 1 + rw [Finset.mul_sum] + exact Finset.sum_congr rfl fun a _ => pow_succ' _ _ + rw [run_ghostSignBody_succ, tsum_probOutput_bind_mul] + have h_ws : ∀ ws : Commit × PrvState, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch)] * + QueryCache.enncard z.2.2) + ≤ (QueryCache.enncard gh + 1) + + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + S := by + intro ws + rw [tsum_probOutput_bind_mul] + have h_ch : ∀ ch : Chal, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch)] * + QueryCache.enncard z.2.2) + ≤ (QueryCache.enncard gh + 1) + + Pr[= none | ids.respond pk sk ws.2 ch] * S := by + intro ch + rw [tsum_probOutput_bind_mul] + have h_oz : ∀ oz : Option Resp, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | (match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch) : + ProbComp (Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × + (M × Commit →ₒ Chal).QueryCache)))] * + QueryCache.enncard z.2.2) + ≤ (QueryCache.enncard gh + 1) + (if oz = none then S else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (by simp), add_zero, tsum_probOutput_pure_mul] + exact le_trans (enncard_uncacheQuery_le M gh (msg, ws.1)) le_self_add + | none => + rw [if_pos rfl] + refine le_trans (ih re (gh.cacheQuery (msg, ws.1) ch)) ?_ + exact add_le_add_left + (QueryCache.enncard_cacheQuery_le gh (msg, ws.1) ch) S + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_oz) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by simp [hoz]] + simp [mul_comm] + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ch) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact tsum_congr fun ch => (mul_assoc _ _ _).symm + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ws) ?_ + rw [hSucc] + have : ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + ≤ ENNReal.ofReal p_abort * S := by + calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * S := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * S := + mul_le_mul_left + (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + calc QueryCache.enncard gh + 1 + + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + ≤ QueryCache.enncard gh + 1 + ENNReal.ofReal p_abort * S := + add_le_add_right this _ + _ = QueryCache.enncard gh + (1 + ENNReal.ofReal p_abort * S) := by + rw [add_assoc] + +/-! ### Charge route: per-target ghost-membership increment (sign step) + +The eager bad event fires when an adversarial read hits the ghost cache. The charge route +bounds the bad probability by the *averaged* ghost-membership charge of a *fixed* read +target `mc`, tracked inductively across the run. The signing step is the only step that +writes to the ghost layer, and the lemma below is its effect on that per-target charge. + +The key correctness move (the one that removes the rejection skew of the eager↔lazy +comparison) is to **drop the rejection event**: a rejected attempt writes `(msg, ws.1)` to +the ghost layer, so it creates a *new* hit at `mc` only when `ws.1 = mc.2` and `msg = mc.1`; +charging that draw against `hGuess` directly gives `Pr[ws.1 = mc.2] ≤ ε` (no `1/Pr[reject]` +conditioning). An *accepted* attempt only `uncacheQuery`-removes a ghost point, so it can +only *decrease* the membership charge. Summed over the `≤ ∑_{a ids.commit pk sk] ≤ ENNReal.ofReal ε) + (mc : M × Commit) : + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (if ((msg, ws.1) : M × Commit) = mc then (1 : ℝ≥0∞) else 0) + ≤ ENNReal.ofReal ε := by + classical + refine le_trans ?_ (hGuess mc.2) + rw [probOutput_map_eq_tsum_ite (ids.commit pk sk) Prod.fst mc.2] + refine ENNReal.tsum_le_tsum fun ws => ?_ + obtain ⟨w, st⟩ := ws + by_cases hhit : ((msg, w) : M × Commit) = mc + · rw [if_pos hhit, mul_one, if_pos (by rw [← hhit])] + · rw [if_neg hhit, mul_zero]; exact zero_le + +omit [SampleableType Stmt] in +/-- **(a) Sign-step ghost-membership charge increment.** Running `ghostSignBody` for `n` +attempts raises the averaged membership charge at a fixed target `mc` by at most +`(∑_{a ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (mc : M × Commit) : + ∀ (n : ℕ) (re gh : (M × Commit →ₒ Chal).QueryCache), + ∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | (ghostSignBody ids M pk sk msg n).run (re, gh)] * memCharge M z.2.2 mc + ≤ memCharge M gh mc + (∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ a) + * ENNReal.ofReal ε := by + intro n + induction n with + | zero => + intro re gh + simp only [ghostSignBody, StateT.run_pure, tsum_probOutput_pure_mul, Finset.range_zero, + Finset.sum_empty, zero_mul, add_zero, le_refl] + | succ n ih => + intro re gh + classical + set S : ℝ≥0∞ := ∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ a with hS + have hSucc : (∑ a ∈ Finset.range (n + 1), ENNReal.ofReal p_abort ^ a) * ENNReal.ofReal ε = + ENNReal.ofReal ε + (ENNReal.ofReal p_abort * S) * ENNReal.ofReal ε := by + rw [Finset.sum_range_succ', pow_zero, add_comm, add_mul, one_mul] + congr 2 + rw [Finset.mul_sum] + exact Finset.sum_congr rfl fun a _ => pow_succ' _ _ + rw [run_ghostSignBody_succ, tsum_probOutput_bind_mul] + have h_ws : ∀ ws : Commit × PrvState, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch)] * + memCharge M z.2.2 mc) + ≤ (memCharge M gh mc + (if ((msg, ws.1) : M × Commit) = mc then 1 else 0)) + + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + (S * ENNReal.ofReal ε) := by + intro ws + rw [tsum_probOutput_bind_mul] + have h_ch : ∀ ch : Chal, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch)] * + memCharge M z.2.2 mc) + ≤ (memCharge M gh mc + (if ((msg, ws.1) : M × Commit) = mc then 1 else 0)) + + Pr[= none | ids.respond pk sk ws.2 ch] * (S * ENNReal.ofReal ε) := by + intro ch + rw [tsum_probOutput_bind_mul] + have h_oz : ∀ oz : Option Resp, + (∑' z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= z | (match oz with + | some z => + pure (some (ws.1, z), + (re.cacheQuery (msg, ws.1) ch, uncacheQuery M gh (msg, ws.1))) + | none => + (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, ws.1) ch) : + ProbComp (Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × + (M × Commit →ₒ Chal).QueryCache)))] * + memCharge M z.2.2 mc) + ≤ (memCharge M gh mc + (if ((msg, ws.1) : M × Commit) = mc then 1 else 0)) + + (if oz = none then S * ENNReal.ofReal ε else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (Option.some_ne_none z), add_zero, tsum_probOutput_pure_mul] + exact le_trans (memCharge_uncacheQuery_le M gh (msg, ws.1) mc) le_self_add + | none => + rw [if_pos rfl] + refine le_trans (ih re (gh.cacheQuery (msg, ws.1) ch)) ?_ + exact add_le_add (memCharge_cacheQuery_le M gh (msg, ws.1) ch mc) le_rfl + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_oz) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by simp [hoz]] + simp [mul_comm] + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ch) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact tsum_congr fun ch => (mul_assoc _ _ _).symm + refine le_trans (ENNReal.tsum_le_tsum fun ws => mul_le_mul_right (h_ws ws) _) ?_ + rw [hSucc] + rw [show (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ((memCharge M gh mc + (if ((msg, ws.1) : M × Commit) = mc then 1 else 0)) + + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + (S * ENNReal.ofReal ε))) = + ∑' ws : Commit × PrvState, (Pr[= ws | ids.commit pk sk] * memCharge M gh mc + + (Pr[= ws | ids.commit pk sk] * + (if ((msg, ws.1) : M × Commit) = mc then 1 else 0) + + Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + (S * ENNReal.ofReal ε)))) from tsum_congr fun ws => by ring, + ENNReal.tsum_add, ENNReal.tsum_add] + refine add_le_add ?_ (add_le_add ?_ ?_) + · rw [ENNReal.tsum_mul_right] + exact mul_le_of_le_one_left (zero_le) tsum_probOutput_le_one + · exact tsum_probOutput_commit_mul_writeHit_le ids M pk sk msg hGuess mc + · calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * (S * ENNReal.ofReal ε)) + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * (S * ENNReal.ofReal ε) := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * (S * ENNReal.ofReal ε) := + mul_le_mul_left (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + _ = ENNReal.ofReal p_abort * S * ENNReal.ofReal ε := by rw [mul_assoc] + +omit [SampleableType Stmt] in +/-- **(b) Eager read-step charge bound.** At any starting state `p`, the eager read's +contribution to the telescoped bad average is at most the per-target ghost-membership charge +`memCharge (p.1.1.2) mc` *plus* the miss-branch continuation charge. + +This is the read-step half of the charge route. On a ghost **hit** (`p.1.1.2 mc = some v`) +the eager read forces the bad flag and the contribution is the continuation success mass +`≤ 1 = memCharge` (the membership indicator is `1`), and the miss branch contributes `0`. On +a **miss** the contribution is exactly the miss-branch continuation and `memCharge = 0`. Thus +the read pays at most `memCharge` above its miss-branch continuation — and the `memCharge` +term is precisely what the averaged charge invariant +(`tsum_probOutput_run_ghostSignBody_mul_memCharge_le`) bounds by `attempts · ε`. Built on the +HIT/MISS split `tsum_ghostHybridImpl_read_step_split`. -/ +lemma tsum_ghostHybridImpl_read_step_charge_le + (pk : Stmt) (sk : Wit) (mc : M × Commit) + (cont : Chal → OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) + (p : GhostState M Commit Chal) : + (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2]) + ≤ memCharge M p.1.1.2 mc + + (match p.1.1.2 mc with + | some _ => 0 + | none => ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + Pr[fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => + w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run + z.2]) := by + rw [tsum_ghostHybridImpl_read_step_split ids M maxAttempts pk sk mc cont p] + unfold memCharge + cases h : p.1.1.2 mc with + | some v => + rw [if_neg (by simp), add_zero] + exact probEvent_le_one + | none => + rw [if_pos rfl, zero_add] + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/GhostLayer.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/GhostLayer.lean new file mode 100644 index 000000000..bfe30aef3 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/GhostLayer.lean @@ -0,0 +1,434 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.Bodies + +/-! +# Ghost-layer machinery for Fiat-Shamir with aborts: GhostLayer + +The ghost-layer presentation of the reprogramming bodies (`ghostSignBody` +over a two-layer cache) and the ghost-instrumented hybrid handlers +(`ghostHybridImpl` with its monotone bad flag, the ghost-blind handler, and the +run-level hybrid handlers). + +Part of the hybrid signing-body development for the CMA-to-NMA reduction; +`VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies` re-exports all of +its modules and holds the overview docstring. +-/ + +@[expose] public section + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) + +/-! ## Ghost-layer presentation of the reprogramming bodies + +The Prog → Trans hop (`probOutput_hybridExpAtKey_prog_le_trans`) compares two signing +bodies whose caches genuinely differ throughout the run: `progSignBody` programs every +attempt, while `transSignBody` programs only the accepted transcript. The bridge is a +two-layer presentation of the cache: a *real* layer holding the entries both games agree +on, and a *ghost* layer holding the rejected-attempt programmings that only +`progSignBody` performs. `ghostSignBody` acts on the layered state, writing the accepted +transcript to the real layer and each rejected attempt to the ghost layer. + +Two projection lemmas make the deferred-sampling step of the hop precise: + +* overlaying the ghost layer onto the real layer recovers `progSignBody` + (`run_ghostSignBody_overlay`), and +* forgetting the ghost layer recovers the accepted-only programming loop of + `transSignBody` (`run_ghostSignBody_fst`) — a programmed point that is never + subsequently read is distributionally removable. -/ + +/-- Overlay a ghost cache onto a real cache; ghost entries shadow real ones. -/ +def overlayCache (re gh : (M × Commit →ₒ Chal).QueryCache) : + (M × Commit →ₒ Chal).QueryCache := + fun q => (gh q).or (re q) + +/-- Remove a single point from a query cache. -/ +def uncacheQuery (cache : (M × Commit →ₒ Chal).QueryCache) (q : M × Commit) : + (M × Commit →ₒ Chal).QueryCache := + fun q' => if q' = q then none else cache q' + +omit [SampleableType Chal] in +lemma overlayCache_cacheQuery_uncacheQuery + (re gh : (M × Commit →ₒ Chal).QueryCache) (q : M × Commit) (c : Chal) : + overlayCache M (re.cacheQuery q c) (uncacheQuery M gh q) = + (overlayCache M re gh).cacheQuery q c := by + funext q' + by_cases hq : q' = q + · subst hq + simp [overlayCache, uncacheQuery] + · simp [overlayCache, uncacheQuery, hq] + +omit [SampleableType Chal] in +/-- Removing a point from a cache does not increase its live-entry count: the support set +of `uncacheQuery cache q` is a subset of that of `cache`. -/ +lemma toSet_uncacheQuery_subset (cache : (M × Commit →ₒ Chal).QueryCache) (q : M × Commit) : + (uncacheQuery M cache q).toSet ⊆ cache.toSet := by + rintro ⟨t', u'⟩ hmem + rw [QueryCache.mem_toSet] at hmem ⊢ + by_cases ht : t' = q + · subst ht; simp only [uncacheQuery, if_true] at hmem; exact absurd hmem (by simp) + · rwa [uncacheQuery, if_neg ht] at hmem + +omit [SampleableType Chal] in +/-- `uncacheQuery` does not increase the `enncard` resource. -/ +lemma enncard_uncacheQuery_le (cache : (M × Commit →ₒ Chal).QueryCache) (q : M × Commit) : + QueryCache.enncard (uncacheQuery M cache q) ≤ QueryCache.enncard cache := + ENat.toENNReal_mono (Set.encard_le_encard (toSet_uncacheQuery_subset M cache q)) + +omit [SampleableType Chal] in +lemma overlayCache_cacheQuery_ghost + (re gh : (M × Commit →ₒ Chal).QueryCache) (q : M × Commit) (c : Chal) : + overlayCache M re (gh.cacheQuery q c) = (overlayCache M re gh).cacheQuery q c := by + funext q' + by_cases hq : q' = q + · subst hq + simp [overlayCache] + · simp [overlayCache, hq] + +omit [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma overlayCache_apply_ghost_some + {gh : (M × Commit →ₒ Chal).QueryCache} {q : M × Commit} {v : Chal} + (re : (M × Commit →ₒ Chal).QueryCache) (h : gh q = some v) : + overlayCache M re gh q = some v := by + simp [overlayCache, h] + +omit [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma overlayCache_apply_ghost_none + {gh : (M × Commit →ₒ Chal).QueryCache} {q : M × Commit} + (re : (M × Commit →ₒ Chal).QueryCache) (h : gh q = none) : + overlayCache M re gh q = re q := by + simp [overlayCache, h] + +omit [SampleableType Chal] in +lemma overlayCache_cacheQuery_real_of_ghost_none + (re : (M × Commit →ₒ Chal).QueryCache) {gh : (M × Commit →ₒ Chal).QueryCache} + {q : M × Commit} (h : gh q = none) (c : Chal) : + overlayCache M (re.cacheQuery q c) gh = (overlayCache M re gh).cacheQuery q c := by + funext q' + by_cases hq : q' = q + · subst hq + simp [overlayCache, h] + · simp [overlayCache, hq] + +/-- Signing body on the layered cache: run the abort loop privately, recording each +rejected attempt's would-be programming in the ghost layer and programming the accepted +transcript into the real layer (clearing any stale ghost entry at that point). -/ +noncomputable def ghostSignBody (pk : Stmt) (sk : Wit) (msg : M) : + ℕ → StateT ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) + ProbComp (Option (Commit × Resp)) + | 0 => pure none + | n + 1 => do + let (w, st) ← liftM (ids.commit pk sk) + let c ← (liftM (uniformSample Chal) : + StateT ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) + ProbComp Chal) + let oz ← liftM (ids.respond pk sk st c) + match oz with + | some z => + modify fun s => (s.1.cacheQuery (msg, w) c, uncacheQuery M s.2 (msg, w)) + pure (some (w, z)) + | none => + modify fun s => (s.1, s.2.cacheQuery (msg, w) c) + ghostSignBody pk sk msg n + +omit [SampleableType Stmt] in +/-- Overlay projection: `ghostSignBody` with the ghost layer overlaid onto the real +layer is exactly `progSignBody` on the overlaid cache. Rejected-attempt programmings +(ghost writes) and accepted programmings (real writes) both surface as ordinary cache +programmings under the overlay. -/ +lemma run_ghostSignBody_overlay (pk : Stmt) (sk : Wit) (msg : M) : + ∀ (n : ℕ) (re gh : (M × Commit →ₒ Chal).QueryCache), + (fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (zs.1, overlayCache M zs.2.1 zs.2.2)) <$> + (ghostSignBody ids M pk sk msg n).run (re, gh) = + (progSignBody ids M pk sk msg n).run (overlayCache M re gh) + | 0, re, gh => by + simp [ghostSignBody, progSignBody] + | (n + 1), re, gh => by + simp only [ghostSignBody, progSignBody, progSignAttempt, bind_assoc, StateT.run_bind, + OracleComp.liftM_run_StateT, map_bind, pure_bind, StateT.run_modify] + refine congrArg (ids.commit pk sk >>= ·) (funext fun wst => ?_) + obtain ⟨w, st⟩ := wst + refine congrArg (uniformSample Chal >>= ·) (funext fun c => ?_) + refine congrArg (ids.respond pk sk st c >>= ·) (funext fun oz => ?_) + cases oz with + | some z => + simp only [StateT.run_bind, StateT.run_modify, pure_bind, StateT.run_pure, + map_pure, overlayCache_cacheQuery_uncacheQuery] + | none => + simp only [StateT.run_bind, StateT.run_modify, pure_bind, + run_ghostSignBody_overlay pk sk msg n re (gh.cacheQuery (msg, w) c), + overlayCache_cacheQuery_ghost] + +omit [SampleableType Stmt] in +/-- Ghost-forgetting projection (deferred sampling): dropping the ghost layer from +`ghostSignBody` yields the accepted-only programming loop of `transSignBody`. The +programming of a rejected attempt lives only in the ghost layer, so removing it does not +change the joint distribution of the signing output and the real cache. -/ +lemma run_ghostSignBody_fst (pk : Stmt) (sk : Wit) (msg : M) : + ∀ (n : ℕ) (re gh : (M × Commit →ₒ Chal).QueryCache), + (fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => (zs.1, zs.2.1)) <$> + (ghostSignBody ids M pk sk msg n).run (re, gh) = + (liftM (firstSome (ids.honestExecution pk sk) n) >>= signProgramCont M msg).run re + | 0, re, gh => by + simp [ghostSignBody, firstSome, signProgramCont] + | (n + 1), re, gh => by + simp only [ghostSignBody, firstSome_succ, IdenSchemeWithAbort.honestExecution, + bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, map_bind, pure_bind] + refine congrArg (ids.commit pk sk >>= ·) (funext fun wst => ?_) + obtain ⟨w, st⟩ := wst + refine congrArg (uniformSample Chal >>= ·) (funext fun c => ?_) + refine congrArg (ids.respond pk sk st c >>= ·) (funext fun oz => ?_) + cases oz with + | some z => + simp only [Option.map_some, StateT.run_bind, StateT.run_modify, pure_bind, + StateT.run_pure, map_pure, signProgramCont] + | none => + simp only [Option.map_none, StateT.run_bind, StateT.run_modify, pure_bind] + rw [run_ghostSignBody_fst pk sk msg n re (gh.cacheQuery (msg, w) c)] + simp only [IdenSchemeWithAbort.honestExecution, StateT.run_bind, + OracleComp.liftM_run_StateT, bind_assoc, pure_bind] + +omit [SampleableType Stmt] in +/-- `run_ghostSignBody_fst` at the attempt budget of the scheme: forgetting the ghost +layer of `ghostSignBody` recovers `transSignBody` on the real layer. -/ +lemma run_ghostSignBody_fst_eq_transSignBody (pk : Stmt) (sk : Wit) (msg : M) + (re gh : (M × Commit →ₒ Chal).QueryCache) : + (fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => (zs.1, zs.2.1)) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run (re, gh) = + (transSignBody ids M maxAttempts pk sk msg).run re := + run_ghostSignBody_fst ids M pk sk msg maxAttempts re gh + +omit [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma overlayCache_empty (re : (M × Commit →ₒ Chal).QueryCache) : + overlayCache M re ∅ = re := by + funext q + simp [overlayCache] + +/-! ## Ghost-instrumented hybrid handlers + +Run-level counterpart of the ghost-layer presentation: handlers for the adversary's +oracles over the layered cache, a signed-message list, and a monotone bad flag that +fires exactly when the adversary's random-oracle query hits a point of the ghost layer. +The Prog-side handler (`ghostHybridImpl … true`) answers such a query from the ghost +layer (matching `progSignBody`'s overlaid cache), while the Trans-side handler +(`ghostHybridImpl … false`) answers it from the real layer (matching `transSignBody`'s +cache). On every other query the two handlers are literally identical, which is the +identical-until-bad shape of `tvDist_simulateQ_run_le_probEvent_output_bad`. -/ + +/-- One caching random-oracle step on a bare cache, as a `ProbComp`. Agrees with +`(randomOracle mc).run re` (see `randomOracle_run_eq_roStep`). -/ +noncomputable def roStep (re : (M × Commit →ₒ Chal).QueryCache) (mc : M × Commit) : + ProbComp (Chal × (M × Commit →ₒ Chal).QueryCache) := + match re mc with + | some v => pure (v, re) + | none => do + let c ← uniformSample Chal + pure (c, re.cacheQuery mc c) + +omit [SampleableType Stmt] in +lemma randomOracle_run_eq_roStep (re : (M × Commit →ₒ Chal).QueryCache) (mc : M × Commit) : + ((randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) mc).run re = + roStep M re mc := by + rw [randomOracle.apply_eq] + cases hre : re mc with + | some v => simp [roStep, hre] + | none => simp [roStep, hre, StateT.run_bind] + +/-- The state of the ghost-instrumented hybrid run: layered cache, signed-message list, +and the bad flag for adversarial reads of the ghost layer. -/ +abbrev GhostState (M Commit Chal : Type) : Type := + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) × Bool + +/-- Instrumented handler for the adversary's oracles over the layered cache. +`progSide` selects the answer at a ghost hit: the ghost value (Prog side) or a fresh +caching read of the real layer (Trans side). The bad flag fires on ghost hits and is +otherwise preserved; signing queries run `ghostSignBody` on the cache layers. -/ +noncomputable def ghostHybridImpl (progSide : Bool) (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (GhostState M Commit Chal) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + match s.1.1.2 mc with + | some v => + if progSide then pure (v, (s.1, true)) + else (fun cu => (cu.1, (((cu.2, s.1.1.2), s.1.2), true))) <$> roStep M s.1.1.1 mc + | none => + (fun cu => (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2))) <$> roStep M s.1.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc => (alc.1, ((alc.2, msg :: s.1.2), s.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run s.1.1 + +omit [SampleableType Stmt] in +/-- The two ghost-instrumented handlers agree on all transitions that leave the bad flag +unset: they differ only in the answer at a ghost hit, which fires the flag on both +sides. -/ +lemma ghostHybridImpl_agree_good (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) + (u : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) + (s' : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) : + Pr[= (u, (s', false)) | + (ghostHybridImpl ids M maxAttempts true pk sk t).run (s, false)] = + Pr[= (u, (s', false)) | + (ghostHybridImpl ids M maxAttempts false pk sk t).run (s, false)] := by + rcases t with (n | mc) | msg + · rfl + · simp only [ghostHybridImpl, StateT.run_mk] + cases hgh : s.1.2 mc with + | some v => + simp only [↓reduceIte] + rw [probOutput_eq_zero_of_not_mem_support (by + intro h + rw [support_pure] at h + simpa using congrArg (fun z : _ × GhostState M Commit Chal => z.2.2) h), + probOutput_eq_zero_of_not_mem_support (by + intro h + rw [if_neg Bool.false_ne_true, support_map] at h + obtain ⟨cu, -, hcu⟩ := h + simpa using congrArg (fun z : _ × GhostState M Commit Chal => z.2.2) hcu)] + | none => rfl + · rfl + +omit [SampleableType Stmt] in +/-- The ghost-instrumented handlers never unset the bad flag. -/ +lemma ghostHybridImpl_bad_mono (progSide : Bool) (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (p : GhostState M Commit Chal) (hp : p.2 = true) : + ∀ z ∈ support ((ghostHybridImpl ids M maxAttempts progSide pk sk t).run p), + z.2.2 = true := by + intro z hz + rcases t with (n | mc) | msg + · simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨_, _, rfl⟩ := hz + exact hp + · simp only [ghostHybridImpl, StateT.run_mk] at hz + rcases hgh : p.1.1.2 mc with _ | v + · simp only [hgh, support_map] at hz + obtain ⟨_, _, rfl⟩ := hz + exact hp + · simp only [hgh] at hz + cases progSide with + | true => + simp only [↓reduceIte, support_pure, Set.mem_singleton_iff] at hz + subst hz + rfl + | false => + rw [if_neg Bool.false_ne_true, support_map] at hz + obtain ⟨_, _, rfl⟩ := hz + rfl + · simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨_, _, rfl⟩ := hz + exact hp + +/-! ### Ghost-blind handler + +The ghost-blind handler `ghostBlindImpl` is the eager hybrid handler `ghostHybridImpl … true` +with one change at the adversarial random-oracle read step: on a ghost-cache *hit* it still +*records* the would-hit by flipping the bad flag, but it *answers* the read from the real +cache via `roStep` — exactly as it does on a *miss* — instead of returning the ghost value. +So the ghost-key values are pure side-data: they are consulted only to set the bad flag and +never influence the run's behaviour. Structurally this is the Trans-side instrumented handler +`ghostHybridImpl … false`, whose hit branch already answers from the real layer while flipping +the flag; `ghostBlindImpl` is a named alias for that handler, isolating its ghost-blind role +in the read-bound spine from the Prog→Trans hop usage of the Trans handler. -/ + +/-- Ghost-blind hybrid handler: identical to `ghostHybridImpl … true` except that an +adversarial random-oracle read at a ghost-cache hit answers from the real layer (`roStep`, +the same as a miss) while still flipping the bad flag. The ghost value never influences the +run. Definitionally the Trans-side handler `ghostHybridImpl … false` +(`ghostBlindImpl_eq_ghostHybridImpl_false`). -/ +noncomputable def ghostBlindImpl (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (GhostState M Commit Chal) ProbComp) := + ghostHybridImpl ids M maxAttempts false pk sk + +omit [SampleableType Stmt] in +/-- The ghost-blind handler is, by definition, the Trans-side instrumented handler. -/ +lemma ghostBlindImpl_eq_ghostHybridImpl_false (pk : Stmt) (sk : Wit) : + ghostBlindImpl ids M maxAttempts pk sk = + ghostHybridImpl ids M maxAttempts false pk sk := rfl + +omit [SampleableType Stmt] in +/-- The eager (`ghostHybridImpl … true`) and ghost-blind handlers agree on every non-bad +output transition from a non-bad input state: they coincide on uniform, signing, and +ghost-*miss* read steps, and on a ghost-*hit* both flip the bad flag, so neither has any +non-bad output there. This is the `h_agree_good` premise of the exact identical-until-bad +machinery. -/ +lemma ghostBlindImpl_agree_good (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) + (u : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) + (s' : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) : + Pr[= (u, (s', false)) | + (ghostHybridImpl ids M maxAttempts true pk sk t).run (s, false)] = + Pr[= (u, (s', false)) | + (ghostBlindImpl ids M maxAttempts pk sk t).run (s, false)] := + ghostHybridImpl_agree_good ids M maxAttempts pk sk t s u s' + +omit [SampleableType Stmt] in +/-- The ghost-blind handler never unsets the bad flag (bad-input monotonicity). -/ +lemma ghostBlindImpl_bad_mono (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (p : GhostState M Commit Chal) (hp : p.2 = true) : + ∀ z ∈ support ((ghostBlindImpl ids M maxAttempts pk sk t).run p), + z.2.2 = true := + ghostHybridImpl_bad_mono ids M maxAttempts false pk sk t p hp + +/-! ## Hybrid run-level handlers -/ + +/-- Run a cache-level action inside the hybrid state (random-oracle cache plus the list +of signed messages), acting on the cache component. -/ +def onCache {α : Type} + (action : StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp α) : + StateT ((M × Commit →ₒ Chal).QueryCache × List M) ProbComp α := + fun s => (fun (a, c) => (a, (c, s.2))) <$> action.run s.1 + +/-- Handler for the adversary's base oracles in the hybrid games: uniform queries are +forwarded and random-oracle queries go through the caching random oracle on the cache +component of the hybrid state. -/ +noncomputable def hybridBaseImpl : + QueryImpl (unifSpec + (M × Commit →ₒ Chal)) + (StateT ((M × Commit →ₒ Chal).QueryCache × List M) ProbComp) := + let base : QueryImpl (unifSpec + (M × Commit →ₒ Chal)) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) := + unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) + fun t => onCache M (base t) + +/-- Handler for the adversary's signing oracle in the hybrid games: record the signed +message (for the freshness check) and run the given signing body on the cache. -/ +noncomputable def hybridSignImpl + (signBody : M → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp))) : + QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT ((M × Commit →ₒ Chal).QueryCache × List M) ProbComp) := + fun msg => do + modify fun s => (s.1, msg :: s.2) + onCache M (signBody msg) + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/NMAHandler.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/NMAHandler.lean new file mode 100644 index 000000000..8b8e6bef9 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/NMAHandler.lean @@ -0,0 +1,423 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.BodyBounds + +/-! +# Ghost-layer machinery for Fiat-Shamir with aborts: NMAHandler + +The layered ghost-tagged NMA handler, programming accepted transcripts +into the ghost layer, with the live-read / sign-program collision event. + +Part of the hybrid signing-body development for the CMA-to-NMA reduction; +`VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies` re-exports all of +its modules and holds the overview docstring. +-/ + +@[expose] public section + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) + +/-! ## Layered ghost-tagged NMA handler + +The NMA bridge (`hybridSimRun_le_managedRun_verify`) couples the single-cache simulated +hybrid against the linked managed run. The obstruction recorded there is that the single +hybrid cache does not, on its own, record whether a point entered by a *live RO read* or by +the *signing simulation's programming* (`signProgramCont`). We resolve this exactly as in +the Prog → Trans hop: run the hybrid on an enriched, layered cache state +`((baseCache, ghostCache), signed)` that tags each entry as live-read (base) vs +signing-programmed (ghost). The base oracles write live RO reads to `baseCache`; the +signing body's `signProgramCont` writes the accepted-transcript programming to `ghostCache`. + +On that layered state the partition *is* a function of the state, so the overlay projection +`((base, ghost), signed) ↦ (overlayCache base ghost, signed)` back to the plain single-cache +hybrid is a per-step state projection in the sense of +`OracleComp.map_run_simulateQ_eq_of_query_map_eq`. This section builds the layered handler +and proves that overlay projection (sub-lemma (a) of the bridge). -/ + +/-- Ghost-layer programming continuation: like `signProgramCont`, but the accepted +transcript's challenge is written to the *ghost* layer of a `(base, ghost)` cache pair. +An all-abort loop outcome produces no signature and no programming. -/ +noncomputable def ghostSignProgramCont (msg : M) : + Option (Commit × Chal × Resp) → + StateT ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) + ProbComp (Option (Commit × Resp)) + | some (w, c, z) => do + modify fun s => (s.1, s.2.cacheQuery (msg, w) c) + pure (some (w, z)) + | none => pure none + +/-- Signing body of the simulated hybrid on the layered cache: run the simulator loop +privately, programming the accepted transcript into the *ghost* layer (`ghostSignProgramCont`). +The base layer is untouched. -/ +noncomputable def simGhostSignBody (pk : Stmt) (msg : M) : + StateT ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) + ProbComp (Option (Commit × Resp)) := + liftM (firstSome (sim pk) maxAttempts) >>= ghostSignProgramCont M msg + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- Overlay projection of `ghostSignProgramCont`: overlaying the ghost layer onto the base +layer turns the ghost-layer programming into an ordinary cache programming, recovering +`signProgramCont` on the overlaid cache. -/ +lemma run_ghostSignProgramCont_overlay (msg : M) + (oz : Option (Commit × Chal × Resp)) + (re gh : (M × Commit →ₒ Chal).QueryCache) : + (fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (zs.1, overlayCache M zs.2.1 zs.2.2)) <$> + (ghostSignProgramCont M msg oz).run (re, gh) = + (signProgramCont M msg oz).run (overlayCache M re gh) := by + cases oz with + | none => simp [ghostSignProgramCont, signProgramCont] + | some wcz => + obtain ⟨w, c, z⟩ := wcz + simp only [ghostSignProgramCont, signProgramCont, StateT.run_bind, StateT.run_modify, + pure_bind, StateT.run_pure, map_pure, overlayCache_cacheQuery_ghost] + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- Overlay projection of `simGhostSignBody`: overlaying the ghost layer onto the base layer +recovers `simSignBody` on the overlaid cache. The simulator loop is run identically; only +the destination layer of the accepted programming differs, which the overlay erases. -/ +lemma run_simGhostSignBody_overlay (pk : Stmt) (sk : Wit) (msg : M) + (re gh : (M × Commit →ₒ Chal).QueryCache) : + (fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (zs.1, overlayCache M zs.2.1 zs.2.2)) <$> + (simGhostSignBody M maxAttempts sim pk msg).run (re, gh) = + (simSignBody M maxAttempts sim pk sk msg).run (overlayCache M re gh) := by + simp only [simGhostSignBody, simSignBody, StateT.run_bind, OracleComp.liftM_run_StateT, + bind_assoc, pure_bind, map_bind] + refine congrArg (firstSome (sim pk) maxAttempts >>= ·) (funext fun oz => ?_) + exact run_ghostSignProgramCont_overlay M msg oz re gh + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- Ghost-domain support fact for `simGhostSignBody`: every ghost-layer entry of an output +state is either an entry already present in the input ghost layer, or sits at a point whose +message component equals the signed message `msg`. The base layer is never touched. -/ +lemma simGhostSignBody_support_ghost (pk : Stmt) (msg : M) + (re gh : (M × Commit →ₒ Chal).QueryCache) + (z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache)) + (hz : z ∈ support ((simGhostSignBody M maxAttempts sim pk msg).run (re, gh))) + (q : M × Commit) (hq : z.2.2 q ≠ none) : gh q ≠ none ∨ q.1 = msg := by + simp only [simGhostSignBody, StateT.run_bind, OracleComp.liftM_run_StateT, bind_assoc, + pure_bind, support_bind, Set.mem_iUnion, exists_prop] at hz + obtain ⟨oz, -, hz⟩ := hz + cases oz with + | none => + simp only [ghostSignProgramCont, StateT.run_pure, support_pure, + Set.mem_singleton_iff] at hz + subst hz + exact Or.inl hq + | some wcz => + obtain ⟨w, c, z'⟩ := wcz + simp only [ghostSignProgramCont, StateT.run_bind, StateT.run_modify, pure_bind, + StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz + simp only at hq + by_cases hqw : q = (msg, w) + · exact Or.inr (by simp [hqw]) + · exact Or.inl (by rwa [QueryCache.cacheQuery_of_ne _ _ hqw] at hq) + +/-- State of the layered ghost-tagged NMA run: a base/ghost cache pair together with the +signed-message list. (No bad flag is needed for the NMA bridge: the coupling is exact, not +identical-until-bad.) -/ +abbrev NmaGhostState (M Commit Chal : Type) : Type := + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M + +/-- Embed a base random-oracle cache (keyed by `M × Commit`) into the outer runtime cache of +the linked managed run, which is keyed by the sum spec `unifSpec + (M × Commit →ₒ Chal)`. The +uniform-query slots are empty (the runtime forwards uniform queries through `unifFwdImpl` +without caching), and the random-oracle slots carry the base entries. This is the left +component of the linked-run projection `proj₂` for sub-lemma (b). -/ +def baseEmbed (base : (M × Commit →ₒ Chal).QueryCache) : + (unifSpec + (M × Commit →ₒ Chal)).QueryCache + | .inl _ => none + | .inr mc => base mc + +omit [SampleableType Stmt] [DecidableEq Commit] [DecidableEq M] [SampleableType Chal] in +@[simp] lemma baseEmbed_inr (base : (M × Commit →ₒ Chal).QueryCache) (mc : M × Commit) : + baseEmbed M base (.inr mc) = base mc := rfl + +omit [SampleableType Stmt] [DecidableEq Commit] [DecidableEq M] [SampleableType Chal] in +@[simp] lemma baseEmbed_inl (base : (M × Commit →ₒ Chal).QueryCache) + (n : unifSpec.Domain) : baseEmbed M base (.inl n) = none := rfl + +omit [SampleableType Stmt] [DecidableEq Commit] [DecidableEq M] [SampleableType Chal] in +/-- The embedding of the empty base cache is the empty outer cache. -/ +@[simp] lemma baseEmbed_empty : + baseEmbed M (∅ : (M × Commit →ₒ Chal).QueryCache) = + (∅ : (unifSpec + (M × Commit →ₒ Chal)).QueryCache) := by + funext t + cases t with + | inl n => rfl + | inr mc => rfl + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- Embedding commutes with caching a random-oracle point: `baseEmbed` of a base cache +extended at `mc` equals the outer cache extended at the `.inr mc` slot. -/ +lemma baseEmbed_cacheQuery (base : (M × Commit →ₒ Chal).QueryCache) + (mc : M × Commit) (v : Chal) : + baseEmbed M (base.cacheQuery mc v) = + (baseEmbed M base).cacheQuery (.inr mc) v := by + funext t + cases t with + | inl n => + rw [QueryCache.cacheQuery_of_ne _ _ (show (Sum.inl n : (unifSpec + + (M × Commit →ₒ Chal)).Domain) ≠ Sum.inr mc by simp), baseEmbed_inl, baseEmbed_inl] + | inr mc' => + by_cases h : mc' = mc + · subst h; simp [baseEmbed, QueryCache.cacheQuery_self] + · rw [baseEmbed_inr, QueryCache.cacheQuery_of_ne _ _ h, + QueryCache.cacheQuery_of_ne _ _ (show (Sum.inr mc' : (unifSpec + + (M × Commit →ₒ Chal)).Domain) ≠ Sum.inr mc by simp [h]), baseEmbed_inr] + +/-- Layered ghost-tagged handler for the simulated hybrid. Base oracles (uniform and the +caching random oracle) write live RO reads to the *base* layer, reading through the overlay +so that signing-programmed (ghost) points are visible to the adversary; the signing oracle +records the signed message and runs `simGhostSignBody`, writing the accepted-transcript +programming to the *ghost* layer. -/ +noncomputable def ghostNmaImpl (pk : Stmt) (_sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (NmaGhostState M Commit Chal) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + match s.1.2 mc with + | some v => pure (v, s) + | none => + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((cu.2, s.1.2), s.2))) <$> roStep M s.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (alc.1, (alc.2, msg :: s.2))) <$> + (simGhostSignBody M maxAttempts sim pk msg).run s.1 + +omit [SampleableType Stmt] in +lemma ghostNmaImpl_run_unif (pk : Stmt) (sk : Wit) (n : unifSpec.Domain) + (s : NmaGhostState M Commit Chal) : + (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inl n))).run s = + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + +omit [SampleableType Stmt] in +lemma ghostNmaImpl_run_ro (pk : Stmt) (sk : Wit) (mc : M × Commit) + (s : NmaGhostState M Commit Chal) : + (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inr mc))).run s = + match s.1.2 mc with + | some v => pure (v, s) + | none => + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((cu.2, s.1.2), s.2))) <$> roStep M s.1.1 mc := rfl + +omit [SampleableType Stmt] in +lemma ghostNmaImpl_run_sign (pk : Stmt) (sk : Wit) (msg : M) + (s : NmaGhostState M Commit Chal) : + (ghostNmaImpl M maxAttempts sim pk sk (.inr msg)).run s = + (fun alc : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (alc.1, (alc.2, msg :: s.2))) <$> + (simGhostSignBody M maxAttempts sim pk msg).run s.1 := rfl + +omit [SampleableType Stmt] in +/-- **Ghost-domain invariant for `ghostNmaImpl`.** Along any run of the layered NMA handler, +every ghost-layer entry's message component has been recorded in the signed list. This is the +NMA analogue of `ghostHybridImpl_preserves_signed_inv`: the base oracles never write the ghost +layer, and the signing oracle records the signed message before programming the ghost layer at +a point whose message component equals that signed message (`simGhostSignBody_support_ghost`). +This invariant is the gate for the linked-run coupling (sub-lemma (b)): it certifies that on a +random-oracle step a live read hits the base/outer layer, while the ghost layer only carries +signing-programmed points. -/ +lemma ghostNmaImpl_preserves_signed_inv (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : NmaGhostState M Commit Chal) + (hs : ∀ q : M × Commit, s.1.2 q ≠ none → q.1 ∈ s.2) : + ∀ z ∈ support ((ghostNmaImpl M maxAttempts sim pk sk t).run s), + ∀ q : M × Commit, z.2.1.2 q ≠ none → q.1 ∈ z.2.2 := by + intro z hz + rcases t with (n | mc) | msg + · simp only [ghostNmaImpl, StateT.run_mk, support_map] at hz + obtain ⟨u, -, rfl⟩ := hz + exact hs + · simp only [ghostNmaImpl, StateT.run_mk] at hz + cases hgh : s.1.2 mc with + | some v => + simp only [hgh, support_pure, Set.mem_singleton_iff] at hz + subst hz + exact hs + | none => + simp only [hgh, support_map] at hz + obtain ⟨cu, -, rfl⟩ := hz + exact hs + · simp only [ghostNmaImpl, StateT.run_mk, support_map] at hz + obtain ⟨alc, halc, rfl⟩ := hz + intro q hq + rcases simGhostSignBody_support_ghost M maxAttempts sim pk msg s.1.1 s.1.2 alc halc q hq + with hgh | hmsg + · exact List.mem_cons_of_mem _ (hs q hgh) + · exact hmsg ▸ List.mem_cons_self + +omit [SampleableType Stmt] in +/-- **Whole-run ghost-domain invariant.** Lifting `ghostNmaImpl_preserves_signed_inv` through +the full simulated run via `simulateQ_run_preserves_inv_of_query`: starting from the empty +layered state `((∅, ∅), [])`, every output state `z` in the support of the layered ghost-tagged +NMA run records each ghost-layer point's message in the signed list. This is the support fact +gating the verify-tail split (`hybridVerifyCont_cache_congr`): on a fresh forgery `msg ∉ z.2.2` +the ghost layer misses at every `(msg, w)`, so the overlay agrees with the base layer there. -/ +lemma ghostNmaImpl_run_signed_inv (pk : Stmt) (sk : Wit) {β : Type} + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) β) : + ∀ z ∈ support ((simulateQ (ghostNmaImpl M maxAttempts sim pk sk) oa).run ((∅, ∅), [])), + ∀ q : M × Commit, z.2.1.2 q ≠ none → q.1 ∈ z.2.2 := by + refine simulateQ_run_preserves_inv_of_query + (ghostNmaImpl M maxAttempts sim pk sk) + (inv := fun s => ∀ q : M × Commit, s.1.2 q ≠ none → q.1 ∈ s.2) + (fun t s hs => ghostNmaImpl_preserves_signed_inv M maxAttempts sim pk sk t s hs) + oa ((∅, ∅), []) ?_ + intro q hq + exact absurd rfl hq + +omit [SampleableType Stmt] in +/-- **Sub-lemma (a): overlay projection of the layered NMA handler.** Each step of the +layered ghost-tagged handler `ghostNmaImpl`, projected by overlaying the ghost layer onto the +base layer, equals the corresponding step of the plain single-cache hybrid handler +`hybridBaseImpl + hybridSignImpl simSignBody`. -/ +lemma ghostNmaImpl_proj_hybrid (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : NmaGhostState M Commit Chal) : + Prod.map id (fun g : NmaGhostState M Commit Chal => + (overlayCache M g.1.1 g.1.2, g.2)) <$> + (ghostNmaImpl M maxAttempts sim pk sk t).run s = + ((hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) t).run + (overlayCache M s.1.1 s.1.2, s.2) := by + rcases t with (n | mc) | msg + · simp only [ghostNmaImpl, StateT.run_mk, QueryImpl.add_apply_inl, hybridBaseImpl, + unifFwdImpl, QueryImpl.liftTarget_apply, Functor.map_map] + rfl + · refine Eq.trans ?_ + (hybridBaseImpl_run_ro M mc (overlayCache M s.1.1 s.1.2) s.2).symm + rw [ghostNmaImpl_run_ro] + cases hgh : s.1.2 mc with + | some v => + rw [roStep_of_some M (overlayCache_apply_ghost_some (M := M) s.1.1 hgh)] + simp + | none => + cases hre : s.1.1 mc with + | some v => + rw [roStep_of_some M hre, roStep_of_some M (show overlayCache M + s.1.1 s.1.2 mc = some v by + rw [overlayCache_apply_ghost_none (M := M) s.1.1 hgh, hre])] + simp + | none => + rw [roStep_of_none M hre, roStep_of_none M (show overlayCache M + s.1.1 s.1.2 mc = none by + rw [overlayCache_apply_ghost_none (M := M) s.1.1 hgh, hre])] + simp [overlayCache_cacheQuery_real_of_ghost_none (M := M) s.1.1 hgh] + · refine Eq.trans (b := (fun ac : Option (Commit × Resp) × + (M × Commit →ₒ Chal).QueryCache => (ac.1, (ac.2, msg :: s.2))) <$> + (simSignBody M maxAttempts sim pk sk msg).run + (overlayCache M s.1.1 s.1.2)) ?_ ?_ + · rw [ghostNmaImpl_run_sign, + ← run_simGhostSignBody_overlay M maxAttempts sim pk sk msg s.1.1 s.1.2] + refine (Functor.map_map _ _ _).trans (Eq.symm ?_) + exact (Functor.map_map _ _ _).trans rfl + · exact (hybridSignImpl_run M (simSignBody M maxAttempts sim pk sk) msg + (overlayCache M s.1.1 s.1.2) s.2).symm + +/-! ### Live-read / sign-program collision event (sub-lemma (b) reframe) + +The per-step state-projection equality `hproj2_sign` is *provably impossible*: a +sign-programmed transcript point `(msg, w)` can coincide with a point already written into the +base (live-read) layer by a prior adversary random-oracle query, and the projection +`proj₂ ((base, ghost), signed) = (baseEmbed base, overlayCache base ghost)` cannot recover the +linked managed handler's separate inner/outer cache split from `(base, ghost)` alone at such a +point (a `(msg, w)` in the overlay could have arrived via a live read — present in the outer +cache — or via signing — absent from the outer cache — and the layered state records no flag +distinguishing the two). + +`signLiveCollisionState` is the exact state-level event distinguishing the two runs: at the +*start of a sign step* for message `msg`, the simulator's accepted commitment `w` lands on a +point `(msg, w)` already live in the base layer. Off this event (`base (msg, w) = none` for the +drawn `w`), the sign step's projection *is* a function of the state and the per-step coupling is +exact. On this event the two runs diverge, and — because the headline bound +`hybridSimRun_le_managedRun_verify` is an *inequality* (`≤`) — the divergence may be paid on the +bad side via the commit-guessing charge `probEvent_commit_hit_le` (the base layer's live-read +count times the per-commit guessing bound `ε`), exactly the charge class already used for the +ghost-read collision in `probEvent_ghostRead_bad_le`. -/ + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- The live-read / sign-program collision predicate at a sign step. For a base (live-read) +cache `base` and a drawn accepted commitment `w` for message `msg`, the collision fires iff the +sign-programmed point `(msg, w)` is already present in the base layer (i.e. the simulator's +commitment landed on a previously live-read random-oracle point). This is the *exact* event off +which the layered NMA run and the linked managed run agree: when `base (msg, w) = none` the +sign-step projection `proj₂` is a function of the state, so the per-step coupling is an exact +equality; when it holds the two runs diverge and the gap is charged by +`probEvent_commit_hit_le`. -/ +def signLiveCollision (base : (M × Commit →ₒ Chal).QueryCache) (msg : M) (w : Commit) : Prop := + base (msg, w) ≠ none + +omit [SampleableType Stmt] [SampleableType Chal] [DecidableEq Commit] [DecidableEq M] in +/-- The collision is decidable and its negation is exactly the no-collision hypothesis +`base (msg, w) = none` under which the sign-step projection is exact. -/ +lemma not_signLiveCollision_iff (base : (M × Commit →ₒ Chal).QueryCache) (msg : M) (w : Commit) : + ¬ signLiveCollision M base msg w ↔ base (msg, w) = none := by + simp [signLiveCollision] + +omit [SampleableType Stmt] in +/-- **Sub-lemma (a), full-run form.** The full simulated run of the layered ghost-tagged NMA +handler `ghostNmaImpl`, projected by overlaying the ghost layer onto the base layer, equals +the plain single-cache simulated hybrid run. This lifts the per-step projection +`ghostNmaImpl_proj_hybrid` through `OracleComp.map_run_simulateQ_eq_of_query_map_eq`. -/ +lemma map_run_simulateQ_ghostNmaImpl_overlay {β : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) β) + (s : NmaGhostState M Commit Chal) : + Prod.map id (fun g : NmaGhostState M Commit Chal => + (overlayCache M g.1.1 g.1.2, g.2)) <$> + (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) oa).run s = + (simulateQ (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) oa).run + (overlayCache M s.1.1 s.1.2, s.2) := + map_run_simulateQ_eq_of_query_map_eq + (ghostNmaImpl M maxAttempts sim pk sk) + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) + (fun g => (overlayCache M g.1.1 g.1.2, g.2)) + (ghostNmaImpl_proj_hybrid M maxAttempts sim pk sk) oa s + +omit [SampleableType Stmt] in +/-- **Sub-lemma (a) at the initial empty layered state.** Starting from the empty layered +cache `((∅, ∅), [])`, the overlay-projected layered run equals the plain single-cache hybrid +run started from `(∅, [])` (using `overlayCache _ ∅ = id` to simplify the projected initial +state). -/ +lemma map_run_simulateQ_ghostNmaImpl_overlay_empty {β : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) β) : + Prod.map id (fun g : NmaGhostState M Commit Chal => + (overlayCache M g.1.1 g.1.2, g.2)) <$> + (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) oa).run ((∅, ∅), []) = + (simulateQ (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) oa).run (∅, []) := by + rw [map_run_simulateQ_ghostNmaImpl_overlay M maxAttempts sim pk sk oa ((∅, ∅), [])] + simp only [overlayCache_empty] + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Projections.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Projections.lean new file mode 100644 index 000000000..22e06c8a6 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/GhostBodies/Projections.lean @@ -0,0 +1,399 @@ +/- +Copyright (c) 2026 Quang Dao, Oleksandr Vovkotrub. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Oleksandr Vovkotrub +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies.GhostLayer + +/-! +# Ghost-layer machinery for Fiat-Shamir with aborts: Projections + +Projections of the ghost-instrumented run onto both hybrid games +(`ghostHybridImpl_proj_prog`, `ghostHybridImpl_proj_trans`), ghost-value +independence of the ghost-blind step, and the ghost-domain invariant +`ghostHybridImpl_preserves_signed_inv`. + +Part of the hybrid signing-body development for the CMA-to-NMA reduction; +`VCVio.CryptoFoundations.FiatShamir.WithAbort.GhostBodies` re-exports all of +its modules and holds the overview docstring. +-/ + +@[expose] public section + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) + +/-! ## Projections of the ghost-instrumented run + +The ghost-instrumented run projects onto the Prog hybrid by overlaying the ghost layer +onto the real one, and onto the Trans hybrid by forgetting the ghost layer. Both are +per-step state projections in the sense of `OracleComp.map_run_simulateQ_eq_of_query_map_eq`. -/ + +omit [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma onCache_run {α : Type} + (action : StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp α) + (s : (M × Commit →ₒ Chal).QueryCache × List M) : + (onCache M action).run s = (fun ac : α × (M × Commit →ₒ Chal).QueryCache => + (ac.1, (ac.2, s.2))) <$> action.run s.1 := rfl + +omit [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma hybridSignImpl_run + (signBody : M → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp))) + (msg : M) (c : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (hybridSignImpl M signBody msg).run (c, l) = + (fun ac : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (ac.1, (ac.2, msg :: l))) <$> (signBody msg).run c := rfl + +omit [SampleableType Stmt] in +lemma roStep_of_some {re : (M × Commit →ₒ Chal).QueryCache} {mc : M × Commit} {v : Chal} + (h : re mc = some v) : roStep M re mc = pure (v, re) := by + unfold roStep + rw [h] + +omit [SampleableType Stmt] in +lemma roStep_of_none {re : (M × Commit →ₒ Chal).QueryCache} {mc : M × Commit} + (h : re mc = none) : + roStep M re mc = uniformSample Chal >>= fun c => pure (c, re.cacheQuery mc c) := by + unfold roStep + rw [h] + +omit [SampleableType Stmt] in +lemma hybridBaseImpl_run_ro (mc : M × Commit) + (c : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M (.inr mc)).run (c, l) = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (cu.2, l))) <$> roStep M c mc := by + have h : (onCache M ((unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) (.inr mc))).run (c, l) = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (cu.2, l))) <$> roStep M c mc := by + rw [QueryImpl.add_apply_inr, onCache_run] + exact congrArg (fun x => (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (cu.2, l))) <$> x) (randomOracle_run_eq_roStep M c mc) + exact h + +omit [SampleableType Stmt] in +lemma ghostHybridImpl_run_ro_ghost_some (progSide : Bool) (pk : Stmt) (sk : Wit) + {mc : M × Commit} {s : GhostState M Commit Chal} {v : Chal} + (h : s.1.1.2 mc = some v) : + (ghostHybridImpl ids M maxAttempts progSide pk sk (.inl (.inr mc))).run s = + if progSide then pure (v, (s.1, true)) + else (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), true))) <$> roStep M s.1.1.1 mc := by + change (match s.1.1.2 mc with + | some v => if progSide then pure (v, (s.1, true)) + else (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), true))) <$> roStep M s.1.1.1 mc + | none => (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2))) <$> roStep M s.1.1.1 mc) = _ + rw [h] + +omit [SampleableType Stmt] in +lemma ghostHybridImpl_run_ro_ghost_none (progSide : Bool) (pk : Stmt) (sk : Wit) + {mc : M × Commit} {s : GhostState M Commit Chal} + (h : s.1.1.2 mc = none) : + (ghostHybridImpl ids M maxAttempts progSide pk sk (.inl (.inr mc))).run s = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2))) <$> roStep M s.1.1.1 mc := by + change (match s.1.1.2 mc with + | some v => if progSide then pure (v, (s.1, true)) + else (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), true))) <$> roStep M s.1.1.1 mc + | none => (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2))) <$> roStep M s.1.1.1 mc) = _ + rw [h] + +omit [SampleableType Stmt] in +lemma ghostHybridImpl_run_sign (progSide : Bool) (pk : Stmt) (sk : Wit) + (msg : M) (s : GhostState M Commit Chal) : + (ghostHybridImpl ids M maxAttempts progSide pk sk (.inr msg)).run s = + (fun alc : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (alc.1, ((alc.2, msg :: s.1.2), s.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run s.1.1 := rfl + +/-! ### Ghost-value independence of the ghost-blind step (the value-free foundation) + +The single structural fact powering the read-bound spine: in the ghost-blind +run the stored ghost *values* are pure write-only side-data. `blindStepProj` forgets exactly +the ghost layer, retaining the run's observable component — the step output, the real cache, +the signed-message list, and the bad flag. `blindStepProj_map_ghostBlindImpl_indep` proves +that this observable projection of one ghost-blind step is *independent of the ghost-layer +values*: any two input ghost layers with the same key domain (same `none`-pattern) produce the +same projected step. The bad flag, the only ghost-derived datum that survives the projection, +fires on ghost-cache *membership* alone, so it depends on the domain, never on the values. -/ + +/-- The observable projection of a ghost-blind step: forget the ghost cache layer, retaining the +step output, the real cache, the signed-message list, and the bad flag. Written as the explicit +projection lambda so that `Functor.map_map` composes it uniformly with each per-branch handler. -/ +def blindStepProj + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) : + (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) + × GhostState M Commit Chal → + (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) + × ((M × Commit →ₒ Chal).QueryCache × List M × Bool) := + fun z => (z.1, z.2.1.1.1, z.2.1.2, z.2.2) + +omit [SampleableType Stmt] in +/-- **Value-free foundation (Stage 1).** The observable projection (output, real cache, signed +list, bad flag — see `blindStepProj`) of a single ghost-blind step is *independent of the ghost +layer's values*. For any query `t` and any two input ghost layers `gh₁, gh₂` sharing the real +cache `re`, signed list `l`, bad flag `bf`, and key *domain* (`gh₁ q = none ↔ gh₂ q = none`), +the projected step distributions coincide. + +This is the manifest output-irrelevance of the stored commitment values in `ghostBlindImpl`, +read off branch-by-branch from the real handler: +* uniform queries leave the ghost layer untouched and never inspect it; +* a random-oracle read answers from the *real* layer via `roStep` on **both** the hit and miss + branches (`ghostHybridImpl_run_ro_ghost_some` / `ghostHybridImpl_run_ro_ghost_none`, hit branch + on the Trans side), and only flips the bad flag, whose firing is the membership test + `mc ∈ dom(gh)` — fixed by the shared domain; +* a signing query runs `ghostSignBody`, whose ghost writes never feed back into the output or + the real cache (`run_ghostSignBody_fst`: forgetting the ghost layer recovers the accepted-only + `transSignBody` loop), so the projected step is the same for either input ghost layer. + +The ghost values therefore influence neither the run's outputs nor its read points; they are +consulted only to set the bad flag. Stage 2 defers a single signing query's rejected-attempt +commitment draws to a front block on the strength of this independence; Stage 3 lifts the +deferral through the full `simulateQ` fold. -/ +lemma blindStepProj_map_ghostBlindImpl_indep (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (re gh₁ gh₂ : (M × Commit →ₒ Chal).QueryCache) (l : List M) (bf : Bool) + (hdom : ∀ q, gh₁ q = none ↔ gh₂ q = none) : + blindStepProj M t <$> + (ghostBlindImpl ids M maxAttempts pk sk t).run (((re, gh₁), l), bf) = + blindStepProj M t <$> + (ghostBlindImpl ids M maxAttempts pk sk t).run (((re, gh₂), l), bf) := by + rw [ghostBlindImpl_eq_ghostHybridImpl_false] + rcases t with (n | mc) | msg + · -- Uniform query: the ghost layer is neither read nor written. + simp only [ghostHybridImpl, StateT.run_mk, blindStepProj, Functor.map_map] + · -- Random-oracle read: answer is `roStep` on the real layer in both branches; the only + -- ghost-derived datum is the membership bad flag, fixed by the shared domain. + change (fun z : Chal × GhostState M Commit Chal => (z.1, z.2.1.1.1, z.2.1.2, z.2.2)) <$> + (ghostHybridImpl ids M maxAttempts false pk sk (.inl (.inr mc))).run + (((re, gh₁), l), bf) = + (fun z : Chal × GhostState M Commit Chal => (z.1, z.2.1.1.1, z.2.1.2, z.2.2)) <$> + (ghostHybridImpl ids M maxAttempts false pk sk (.inl (.inr mc))).run + (((re, gh₂), l), bf) + cases hgh1 : gh₁ mc with + | none => + rw [ghostHybridImpl_run_ro_ghost_none ids M maxAttempts false pk sk + (s := (((re, gh₁), l), bf)) hgh1, + ghostHybridImpl_run_ro_ghost_none ids M maxAttempts false pk sk + (s := (((re, gh₂), l), bf)) ((hdom mc).1 hgh1)] + simp [Functor.map_map] + | some v => + have hgh2 : gh₂ mc ≠ none := fun h => + Option.some_ne_none v (hgh1 ▸ (hdom mc).2 h) + obtain ⟨v2, hgh2'⟩ := Option.ne_none_iff_exists'.1 hgh2 + rw [ghostHybridImpl_run_ro_ghost_some ids M maxAttempts false pk sk + (s := (((re, gh₁), l), bf)) hgh1, + ghostHybridImpl_run_ro_ghost_some ids M maxAttempts false pk sk + (s := (((re, gh₂), l), bf)) hgh2', + if_neg Bool.false_ne_true, if_neg Bool.false_ne_true] + simp [Functor.map_map] + · -- Signing query: the ghost writes are forgotten by the projection, and the output plus real + -- cache are value-free by `run_ghostSignBody_fst`. + change (fun z : Option (Commit × Resp) × GhostState M Commit Chal => + (z.1, z.2.1.1.1, z.2.1.2, z.2.2)) <$> + (ghostHybridImpl ids M maxAttempts false pk sk (.inr msg)).run (((re, gh₁), l), bf) = + (fun z : Option (Commit × Resp) × GhostState M Commit Chal => + (z.1, z.2.1.1.1, z.2.1.2, z.2.2)) <$> + (ghostHybridImpl ids M maxAttempts false pk sk (.inr msg)).run (((re, gh₂), l), bf) + rw [ghostHybridImpl_run_sign ids M maxAttempts false pk sk msg (((re, gh₁), l), bf), + ghostHybridImpl_run_sign ids M maxAttempts false pk sk msg (((re, gh₂), l), bf)] + simp only [Functor.map_map] + have h1 := run_ghostSignBody_fst ids M pk sk msg maxAttempts re gh₁ + have h2 := run_ghostSignBody_fst ids M pk sk msg maxAttempts re gh₂ + calc (fun x : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (x.1, x.2.1, msg :: l, bf)) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run (re, gh₁) + = (fun y : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (y.1, y.2, msg :: l, bf)) <$> + ((fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (zs.1, zs.2.1)) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run (re, gh₁)) := by + rw [Functor.map_map] + _ = (fun y : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (y.1, y.2, msg :: l, bf)) <$> + ((fun zs : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (zs.1, zs.2.1)) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run (re, gh₂)) := by + rw [h1, h2] + _ = _ := by rw [Functor.map_map] + +omit [SampleableType Stmt] in +/-- Per-step overlay projection: each step of the Prog-side ghost-instrumented handler +projects onto the corresponding step of the Prog hybrid handler. -/ +lemma ghostHybridImpl_proj_prog (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : GhostState M Commit Chal) : + Prod.map id (fun g : GhostState M Commit Chal => + (overlayCache M g.1.1.1 g.1.1.2, g.1.2)) <$> + (ghostHybridImpl ids M maxAttempts true pk sk t).run s = + ((hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (progSignBody ids M pk sk · maxAttempts)) t).run + (overlayCache M s.1.1.1 s.1.1.2, s.1.2) := by + rcases t with (n | mc) | msg + · simp only [ghostHybridImpl, StateT.run_mk, QueryImpl.add_apply_inl, hybridBaseImpl, + unifFwdImpl, QueryImpl.liftTarget_apply, Functor.map_map] + rfl + · refine Eq.trans ?_ + (hybridBaseImpl_run_ro M mc (overlayCache M s.1.1.1 s.1.1.2) s.1.2).symm + cases hgh : s.1.1.2 mc with + | some v => + rw [ghostHybridImpl_run_ro_ghost_some ids M maxAttempts true pk sk hgh, + if_pos rfl, + roStep_of_some M (overlayCache_apply_ghost_some (M := M) s.1.1.1 hgh)] + simp + | none => + rw [ghostHybridImpl_run_ro_ghost_none ids M maxAttempts true pk sk hgh] + cases hre : s.1.1.1 mc with + | some v => + rw [roStep_of_some M hre, roStep_of_some M (show overlayCache M + s.1.1.1 s.1.1.2 mc = some v by + rw [overlayCache_apply_ghost_none (M := M) s.1.1.1 hgh, hre])] + simp + | none => + rw [roStep_of_none M hre, roStep_of_none M (show overlayCache M + s.1.1.1 s.1.1.2 mc = none by + rw [overlayCache_apply_ghost_none (M := M) s.1.1.1 hgh, hre])] + simp [overlayCache_cacheQuery_real_of_ghost_none (M := M) s.1.1.1 hgh] + · refine Eq.trans (b := (fun ac : Option (Commit × Resp) × + (M × Commit →ₒ Chal).QueryCache => (ac.1, (ac.2, msg :: s.1.2))) <$> + (progSignBody ids M pk sk msg maxAttempts).run + (overlayCache M s.1.1.1 s.1.1.2)) ?_ ?_ + · rw [ghostHybridImpl_run_sign ids M maxAttempts true pk sk msg s, + ← run_ghostSignBody_overlay ids M pk sk msg maxAttempts s.1.1.1 s.1.1.2] + refine (Functor.map_map _ _ _).trans (Eq.symm ?_) + exact (Functor.map_map _ _ _).trans rfl + · exact (hybridSignImpl_run M (progSignBody ids M pk sk · maxAttempts) msg + (overlayCache M s.1.1.1 s.1.1.2) s.1.2).symm + +omit [SampleableType Stmt] in +/-- Per-step ghost-forgetting projection: each step of the Trans-side ghost-instrumented +handler projects onto the corresponding step of the Trans hybrid handler. -/ +lemma ghostHybridImpl_proj_trans (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : GhostState M Commit Chal) : + Prod.map id (fun g : GhostState M Commit Chal => (g.1.1.1, g.1.2)) <$> + (ghostHybridImpl ids M maxAttempts false pk sk t).run s = + ((hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (transSignBody ids M maxAttempts pk sk)) t).run + (s.1.1.1, s.1.2) := by + rcases t with (n | mc) | msg + · simp only [ghostHybridImpl, StateT.run_mk, QueryImpl.add_apply_inl, hybridBaseImpl, + unifFwdImpl, QueryImpl.liftTarget_apply, Functor.map_map] + rfl + · refine Eq.trans ?_ (hybridBaseImpl_run_ro M mc s.1.1.1 s.1.2).symm + cases hgh : s.1.1.2 mc with + | some v => + rw [ghostHybridImpl_run_ro_ghost_some ids M maxAttempts false pk sk hgh, + if_neg Bool.false_ne_true] + exact (Functor.map_map _ _ _).trans rfl + | none => + rw [ghostHybridImpl_run_ro_ghost_none ids M maxAttempts false pk sk hgh] + exact (Functor.map_map _ _ _).trans rfl + · refine Eq.trans (b := (fun ac : Option (Commit × Resp) × + (M × Commit →ₒ Chal).QueryCache => (ac.1, (ac.2, msg :: s.1.2))) <$> + (transSignBody ids M maxAttempts pk sk msg).run s.1.1.1) ?_ ?_ + · rw [ghostHybridImpl_run_sign ids M maxAttempts false pk sk msg s, + ← run_ghostSignBody_fst_eq_transSignBody ids M maxAttempts pk sk msg + s.1.1.1 s.1.1.2] + refine (Functor.map_map _ _ _).trans (Eq.symm ?_) + exact (Functor.map_map _ _ _).trans rfl + · exact (hybridSignImpl_run M (transSignBody ids M maxAttempts pk sk) msg + s.1.1.1 s.1.2).symm + +/-! ## Ghost-domain invariant -/ + +omit [SampleableType Stmt] in +/-- Support bound for the ghost writes of `ghostSignBody`: every ghost entry of an +output state was either already present or lies at the signed message `msg`. -/ +lemma ghostSignBody_support_ghost (pk : Stmt) (sk : Wit) (msg : M) : + ∀ (n : ℕ) (re gh : (M × Commit →ₒ Chal).QueryCache) + (z : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache)), + z ∈ support ((ghostSignBody ids M pk sk msg n).run (re, gh)) → + ∀ q : M × Commit, z.2.2 q ≠ none → gh q ≠ none ∨ q.1 = msg + | 0, re, gh, z, hz, q, hq => by + simp only [ghostSignBody, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz + exact Or.inl hq + | (n + 1), re, gh, z, hz, q, hq => by + simp only [ghostSignBody, StateT.run_bind, OracleComp.liftM_run_StateT, bind_assoc, + pure_bind, support_bind, Set.mem_iUnion, exists_prop] at hz + obtain ⟨⟨w, st⟩, -, hz⟩ := hz + obtain ⟨c, -, hz⟩ := hz + obtain ⟨oz, -, hz⟩ := hz + rcases oz with - | zr + · simp only [StateT.run_bind, StateT.run_modify, pure_bind] at hz + rcases ghostSignBody_support_ghost pk sk msg n re (gh.cacheQuery (msg, w) c) + z hz q hq with hgh | hmsg + · by_cases hqw : q = (msg, w) + · exact Or.inr (by simp [hqw]) + · exact Or.inl (by rwa [QueryCache.cacheQuery_of_ne _ _ hqw] at hgh) + · exact Or.inr hmsg + · simp only [StateT.run_bind, StateT.run_modify, pure_bind, StateT.run_pure, + support_pure, Set.mem_singleton_iff] at hz + subst hz + simp only [uncacheQuery, ne_eq, ite_eq_left_iff, not_forall] at hq + exact Or.inl hq.2 + +omit [SampleableType Stmt] in +/-- Ghost-domain invariant: along any run of the ghost-instrumented handlers, every +ghost entry's message component has been recorded in the signed list. -/ +lemma ghostHybridImpl_preserves_signed_inv (progSide : Bool) (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : GhostState M Commit Chal) + (hs : ∀ q : M × Commit, s.1.1.2 q ≠ none → q.1 ∈ s.1.2) : + ∀ z ∈ support ((ghostHybridImpl ids M maxAttempts progSide pk sk t).run s), + ∀ q : M × Commit, z.2.1.1.2 q ≠ none → q.1 ∈ z.2.1.2 := by + intro z hz + rcases t with (n | mc) | msg + · simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨u, -, rfl⟩ := hz + exact hs + · simp only [ghostHybridImpl, StateT.run_mk] at hz + rcases hgh : s.1.1.2 mc with - | v + · simp only [hgh, support_map] at hz + obtain ⟨cu, -, rfl⟩ := hz + exact hs + · simp only [hgh] at hz + cases progSide with + | true => + simp only [↓reduceIte, support_pure, Set.mem_singleton_iff] at hz + subst hz + exact hs + | false => + rw [if_neg Bool.false_ne_true, support_map] at hz + obtain ⟨cu, -, rfl⟩ := hz + exact hs + · simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨alc, halc, rfl⟩ := hz + intro q hq + rcases ghostSignBody_support_ghost ids M pk sk msg maxAttempts s.1.1.1 s.1.1.2 + alc halc q hq with hgh | hmsg + · exact List.mem_cons_of_mem _ (hs q hgh) + · exact hmsg ▸ List.mem_cons_self + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security.lean index d49eb008e..902e2acce 100644 --- a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security.lean +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security.lean @@ -6,150 +6,46 @@ Authors: Quang Dao module -public import VCVio.CryptoFoundations.FiatShamir.QueryBounds -public import VCVio.CryptoFoundations.FiatShamir.WithAbort +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.Loss +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.BodyHops +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.GhostReadCharge +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.HiddenReadFold +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.CouplingEngine +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.ReadRecording +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.TapeFactorization +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.HopLemmas +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.NMAReduction +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.Assembly /-! # EUF-CMA security of Fiat-Shamir with aborts Statistical CMA-to-NMA reduction for the Fiat-Shamir-with-aborts transform, -matching Theorem 3 of Barbosa et al. (CRYPTO 2023). Instantiates -`FiatShamir.signHashQueryBound` at the with-aborts signature type and exposes -`cmaToNmaLoss` plus `euf_cma_bound` / `euf_cma_bound_perfectHVZK`. +following Theorem 3 of Barbosa et al. (CRYPTO 2023, ePrint 2023/246). +Instantiates `FiatShamir.signHashQueryBound` at the with-aborts signature type +and exposes `cmaToNmaLoss` plus `euf_cma_to_nma` (the managed-RO NMA interface), +together with the hybrid game chain (`hybridExpAtKey` over the signing bodies +`realSignBody`, `progSignBody`, `transSignBody`, `simSignBody`) that structures +the proof. + +The quantitative parameters `ε` (per-key commitment-guessing probability), +`p_abort` (per-attempt abort probability), and `δ` (key-regularity failure +probability) are tied to the identification scheme by explicit hypotheses on a +"good key" event, mirroring the event `Γ` of the paper's Lemma 1: `δ` bounds +the probability that key generation falls outside the event, and `ε`/`p_abort` +bound the per-key quantities pointwise on it. The scheme-specific NMA-to-hard-problem reduction lives with each concrete -scheme (e.g. the ML-DSA MLWE + SelfTargetMSIS reduction). --/ - -@[expose] public section - -universe u v - -open OracleComp OracleSpec - -variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} - -namespace FiatShamirWithAbort - -section EUF_CMA - -variable [SampleableType Stmt] -variable [DecidableEq Commit] [SampleableType Chal] -variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) - (hr : GenerableRelation Stmt Wit rel) - (M : Type) [DecidableEq M] (maxAttempts : ℕ) - -/-- The exact classical ROM statistical loss from the Fiat-Shamir-with-aborts -CMA-to-NMA reduction (Theorem 3, CRYPTO 2023), parameterized by the HVZK simulator -error `ζ_zk`. - -The paper proves - -`Adv_EUF-CMA(A) ≤ Adv_EUF-NMA(B) - + 2·qS·(qH+1)·ε/(1-p) - + qS·ε·(qS+1)/(2·(1-p)^2) - + qS·ζ_zk - + δ` +scheme (e.g. `MLDSA.nma_security_short`). -where: -- `qS`: number of signing-oracle queries -- `qH`: number of adversarial random-oracle queries -- `ε`: commitment-guessing bound -- `p`: effective abort probability -- `ζ_zk`: total-variation error of the HVZK simulator for one signing transcript -- `δ`: regularity failure probability +## Module layout -The `qH + 1` term comes from applying the paper's hybrid bounds to the forging -experiment, which adds one final verification query to the random oracle. -/ -noncomputable def cmaToNmaLoss (qS qH : ℕ) (ε p ζ_zk δ : ℝ) (_hp : p < 1) : ℝ := - 2 * qS * (qH + 1) * ε / (1 - p) + - qS * ε * (qS + 1) / (2 * (1 - p) ^ 2) + - qS * ζ_zk + - δ - -/-- **CMA-to-NMA reduction for Fiat-Shamir with aborts (Theorem 3, CRYPTO 2023).** - -For any EUF-CMA adversary `A` making at most `qS` signing-oracle queries and `qH` -random-oracle queries, there exists an NMA reduction such that: - - `Adv^{EUF-CMA}(A) ≤ Adv^{EUF-NMA}(B) + L` - -The reduction uses: -1. The quantitative HVZK simulator `sim` to answer signing queries without the secret key -2. Commitment recoverability `recover` to map between the standard and commitment-recoverable - variants of the signature scheme -3. Nested hybrid arguments over ROM reprogramming (accepted and rejected transcripts) - -The statistical loss `L` involves the commitment guessing probability `ε`, the effective -abort probability `p`, the simulator error `ζ_zk`, the regularity failure probability `δ`, -and the query bounds `qS`, `qH`; it is captured here by `cmaToNmaLoss`. - -The scheme-specific reduction from NMA to computational assumptions (e.g., MLWE + -SelfTargetMSIS for ML-DSA) is stated separately with each scheme; see -`MLDSA.euf_cma_security`. - -**WARNING: this is a placeholder statement, not the final theorem.** The current shape is -unsound as written: `ε` and `δ : ℝ` are unconstrained signed reals (only `0 ≤ ζ_zk` and -`p_abort < 1` are assumed). Choosing `ε`, `δ` very negative drives `cmaToNmaLoss` into -`(-∞, 0)`; `ENNReal.ofReal` clamps to `0`; the bound collapses to -`adv.advantage ≤ Pr[hard relation reduction]` with no statistical slack, which is generally -false for any non-trivially-secure hard relation. In the final statement `ε` and `δ` should -be nonnegative (e.g. `ℝ≥0` or constrained by `0 ≤ ε`, `0 ≤ δ` hypotheses), and `p_abort` -should additionally be `0 ≤ p_abort` so the divisors `1 - p` and `(1 - p)²` carry their -intended sign. The proof is intentionally deferred. -/ -theorem euf_cma_bound - (hc : ids.Complete) - (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) - (ζ_zk : ℝ) - (hζ : 0 ≤ ζ_zk) - (hhvzk : ids.HVZK sim ζ_zk) - (recover : Stmt → Chal → Resp → Commit) - (hcr : ids.CommitmentRecoverable recover) - (adv : SignatureAlg.unforgeableAdv - (FiatShamirWithAbort - (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) - (qS qH : ℕ) (ε p_abort δ : ℝ) (hp : p_abort < 1) - (hQ : ∀ pk, FiatShamir.signHashQueryBound M - (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) : - ∃ reduction : Stmt → ProbComp Wit, - adv.advantage (runtime M) ≤ - Pr[= true | hardRelationExp hr reduction] + - ENNReal.ofReal (cmaToNmaLoss qS qH ε p_abort ζ_zk δ hp) := by - let _ := hc - let _ := hζ - let _ := hhvzk - let _ := hcr - let _ := hQ - sorry - -/-- Perfect-HVZK special case of `euf_cma_bound`, where the simulator contributes no -`qS · ζ_zk` loss term. - -**WARNING: this is a placeholder statement, not the final theorem.** It inherits the -unsoundness of `euf_cma_bound` (unconstrained signed `ε`, `δ : ℝ`); see that theorem's -docstring. -/ -theorem euf_cma_bound_perfectHVZK - (hc : ids.Complete) - (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) - (hhvzk : ids.PerfectHVZK sim) - (recover : Stmt → Chal → Resp → Commit) - (hcr : ids.CommitmentRecoverable recover) - (adv : SignatureAlg.unforgeableAdv - (FiatShamirWithAbort - (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) - (qS qH : ℕ) (ε p_abort δ : ℝ) (hp : p_abort < 1) - (hQ : ∀ pk, FiatShamir.signHashQueryBound M - (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) : - ∃ reduction : Stmt → ProbComp Wit, - adv.advantage (runtime M) ≤ - Pr[= true | hardRelationExp hr reduction] + - ENNReal.ofReal (cmaToNmaLoss qS qH ε p_abort 0 δ hp) := - euf_cma_bound (ids := ids) (M := M) (maxAttempts := maxAttempts) - (hc := hc) (sim := sim) (ζ_zk := 0) (hζ := le_rfl) - (hhvzk := (IdenSchemeWithAbort.perfectHVZK_iff_hvzk_zero ids sim).mp hhvzk) - (recover := recover) (hcr := hcr) (adv := adv) - (qS := qS) (qH := qH) (ε := ε) (p_abort := p_abort) (δ := δ) (hp := hp) (hQ := hQ) - -end EUF_CMA +The development is split along its proof phases: `Loss` (the loss functions), +`BodyHops` (the per-query Trans → Sim hop core and the verification tail), +`GhostReadCharge`, `HiddenReadFold`, `CouplingEngine` (reusable measure-level +coupling infrastructure, off the live path), `ReadRecording`, +`TapeFactorization`, `HopLemmas`, `NMAReduction`, and `Assembly` (the headline +`euf_cma_to_nma`). This umbrella module re-exports all of them. +-/ -end FiatShamirWithAbort +@[expose] public section diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/Assembly.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/Assembly.lean new file mode 100644 index 000000000..25377d353 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/Assembly.lean @@ -0,0 +1,421 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.NMAReduction + +/-! +# EUF-CMA for Fiat-Shamir with aborts: Assembly + +The bridge from the managed-RO NMA experiment to the plain EUF-NMA +interface (Option B: the forgery's own verification point is discarded from the +returned cache), and the assembled headline `euf_cma_to_nma`. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ## Bridge to the plain EUF-NMA interface + +Option B makes `simulatedNmaAdv` discard the forgery's own verification point from the +returned managed cache. The single hash query issued by `FiatShamirWithAbort.verify` +therefore always *misses* in the overlay and falls through to the live oracle, so the +overlay verification coincides — as an `OracleComp` — with the plain verification. This +collapses the managed-RO NMA experiment onto the plain EUF-NMA experiment of the +cache-forgetting adversary `simulatedEufNmaAdv`, making the bound +`Pr[managedRoNmaExp simulatedNmaAdv] ≤ simulatedEufNmaAdv.advantage` sound. -/ + +/-- The plain EUF-NMA adversary underlying `simulatedNmaAdv`: run the same managed +simulation of the CMA adversary, but forget the returned cache and verify in the plain +random-oracle model. By Option B (`withCacheOverlay_verify_eq_of_miss`) the managed-RO NMA +experiment of `simulatedNmaAdv` coincides with the plain EUF-NMA experiment of this +adversary. -/ +noncomputable def simulatedEufNmaAdv : + SignatureAlg.eufNmaAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts) where + main pk := Prod.fst <$> (simulatedNmaAdv ids hr M maxAttempts sim adv).main pk + +omit [SampleableType Stmt] in +/-- **Soundness of the managed-RO → plain EUF-NMA bridge** (Option B). The managed-RO NMA +success probability of `simulatedNmaAdv` equals the plain EUF-NMA success probability of +`simulatedEufNmaAdv`. The Option B post-processing erases the forgery's own verification +point from the returned cache, so `withCacheOverlay` agrees with the plain live verifier +on every forgery (`withCacheOverlay_verify_eq_of_miss`); in particular a replayed signed +forgery no longer wins through a programmed challenge. -/ +lemma managedRoNmaExp_simulatedNmaAdv_eq_eufNmaExp : + SignatureAlg.managedRoNmaExp (runtime M) + (simulatedNmaAdv ids hr M maxAttempts sim adv) = + SignatureAlg.eufNmaExp (runtime M) + (simulatedEufNmaAdv ids hr M maxAttempts sim adv) := by + unfold SignatureAlg.managedRoNmaExp SignatureAlg.eufNmaExp + refine congrArg (runtime M).evalDist ?_ + refine bind_congr fun pksk => ?_ + -- Reduce the eufNma side `Prod.fst <$> _` to a bind, so both sides bind over + -- `simulatedNmaAdv.main`, then compare the verification wrappers pointwise. + change ((simulatedNmaAdv ids hr M maxAttempts sim adv).main pksk.1 >>= fun result => + withCacheOverlay result.2 + ((FiatShamirWithAbort ids hr M maxAttempts).verify + pksk.1 result.1.1 result.1.2)) = + (Prod.fst <$> (simulatedNmaAdv ids hr M maxAttempts sim adv).main pksk.1) >>= fun ms => + (FiatShamirWithAbort ids hr M maxAttempts).verify pksk.1 ms.1 ms.2 + rw [map_eq_bind_pure_comp, bind_assoc] + -- Unfold `.main` to expose the inner managed run followed by the Option-B + -- post-processing, then `bind_congr` over the inner run. + simp only [simulatedNmaAdv, bind_assoc, pure_bind, Function.comp_apply] + refine bind_congr fun r => ?_ + -- `r.1.2` is the inner forgery's signature; the post-processed cache erases its own + -- verification point, so the overlay verification agrees with the plain verification. + refine withCacheOverlay_verify_eq_of_miss ids hr M maxAttempts _ pksk.1 r.1.1 r.1.2 ?_ + intro w' z hσ + simp only [hσ, Function.update_self] + +/-! ## Assembly -/ + +omit [SampleableType Stmt] in +/-- **CMA-to-NMA reduction for Fiat-Shamir with aborts** (after Theorem 3, CRYPTO 2023), +at the managed-RO NMA interface: for any EUF-CMA adversary making at most `qS` signing +and `qH` hash queries, the CMA advantage is bounded by the managed-RO NMA success +probability of `simulatedNmaAdv` plus the statistical loss `cmaToNmaLoss`. + +The good-key event `Good` plays the role of the event `Γ` in the paper's Lemma 1: `δ` +bounds its complement under key generation, while `ε` and `p_abort` bound the per-key +commitment-guessing and per-attempt abort probabilities pointwise on it. -/ +theorem euf_cma_to_nma + (ζ_zk : ℝ) (hζ : 0 ≤ ζ_zk) (hhvzk : ids.HVZK sim ζ_zk) + (qS qH : ℕ) (ε p_abort δ : ℝ) + (hε : 0 ≤ ε) (hδ : 0 ≤ δ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (Good : Stmt → Wit → Prop) + (hGood : Pr[ fun xw : Stmt × Wit => ¬ Good xw.1 xw.2 | hr.gen] ≤ ENNReal.ofReal δ) + (hGuess : ∀ pk sk, Good pk sk → ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : ∀ pk sk, Good pk sk → + Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (hAbortSim : ∀ pk sk, Good pk sk → + Pr[= none | sim pk] ≤ ENNReal.ofReal p_abort) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) : + adv.advantage (runtime M) ≤ + Pr[= true | SignatureAlg.managedRoNmaExp (runtime M) + (simulatedNmaAdv ids hr M maxAttempts sim adv)] + + ENNReal.ofReal (cmaToNmaLoss qS qH ε p_abort ζ_zk δ hp) := by + classical + -- `advantage = Pr[G₀]` via the per-key bridge `G₀`. + rw [SignatureAlg.unforgeableAdv.advantage, + probOutput_unforgeableExp_eq_hybridExpAtKey_real ids hr M maxAttempts adv] + -- Nonnegativity of the three per-hop slack pieces. + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + have hA : 0 ≤ qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + qS * (qH + 1) * ε / (1 - p_abort) := + add_nonneg + (div_nonneg (by positivity) (by positivity)) + (div_nonneg (by positivity) (le_of_lt h1p)) + have hB : 0 ≤ qS * (qH + 1) * ε / (1 - p_abort) := div_nonneg (by positivity) (le_of_lt h1p) + have hC : 0 ≤ qS * ζ_zk / (1 - p_abort) := div_nonneg (by positivity) (le_of_lt h1p) + have hPK : 0 ≤ perKeyLoss qS qH ε p_abort ζ_zk := by unfold perKeyLoss; positivity + -- Per-key chain on good keys: `real ≤ sim + ofReal (perKeyLoss)`. + have hperkey : ∀ x ∈ support hr.gen, Good x.1 x.2 → + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts x.1 x.2) x.1] ≤ + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1] + + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) := by + rintro ⟨pk, sk⟩ hmem hgood + have hrel : rel pk sk = true := hr.gen_sound pk sk hmem + have step1 := probOutput_hybridExpAtKey_real_le_prog ids hr M maxAttempts adv qS qH ε p_abort + hp₀ hp hQ pk sk (hGuess pk sk hgood) (hAbort pk sk hgood) + have step2 := probOutput_hybridExpAtKey_prog_le_trans ids hr M maxAttempts adv qS qH ε p_abort + hp₀ hp hε hQ pk sk (hGuess pk sk hgood) (hAbort pk sk hgood) + have step3 := probOutput_hybridExpAtKey_trans_le_sim ids hr M maxAttempts sim adv ζ_zk hζ hhvzk + qS qH p_abort hp₀ hp hQ pk sk hrel (hAbortSim pk sk hgood) + -- Chain the three hops and collapse the `ofReal` sums (slack pieces nonneg). + calc Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk] + ≤ Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (fun x ↦ progSignBody ids M pk sk x maxAttempts) pk] + + ENNReal.ofReal (qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + + qS * (qH + 1) * ε / (1 - p_abort)) := step1 + _ ≤ (Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (transSignBody ids M maxAttempts pk sk) pk] + + ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort))) + + ENNReal.ofReal (qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + + qS * (qH + 1) * ε / (1 - p_abort)) := by gcongr + _ ≤ ((Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim pk sk) pk] + + ENNReal.ofReal (qS * ζ_zk / (1 - p_abort))) + + ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort))) + + ENNReal.ofReal (qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + + qS * (qH + 1) * ε / (1 - p_abort)) := by gcongr + _ = Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim pk sk) pk] + + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) := by + have hcollapse : + ENNReal.ofReal (qS * ζ_zk / (1 - p_abort)) + + ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) + + ENNReal.ofReal (qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + + qS * (qH + 1) * ε / (1 - p_abort)) = + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) := by + rw [← ENNReal.ofReal_add hC hB, ← ENNReal.ofReal_add (add_nonneg hC hB) hA] + congr 1 + unfold perKeyLoss + ring + rw [add_assoc, add_assoc, ← add_assoc (ENNReal.ofReal (qS * ζ_zk / (1 - p_abort))), + hcollapse] + -- Average the per-key bound over `hr.gen`, paying `δ` on the complement of `Good`. + have hbound : Pr[= true | do + let x ← hr.gen + hybridExpAtKey ids hr M maxAttempts adv (realSignBody ids M maxAttempts x.1 x.2) x.1] ≤ + Pr[= true | do + let x ← hr.gen + hybridExpAtKey ids hr M maxAttempts adv (simSignBody M maxAttempts sim x.1 x.2) x.1] + + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) + ENNReal.ofReal δ := by + simp only [probOutput_bind_eq_tsum] + -- Pointwise: split on `Good`. On `Good` use `hperkey`; off `Good` charge the `δ` slot. + have hpt : ∀ x : Stmt × Wit, + Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts x.1 x.2) x.1] ≤ + Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1] + + Pr[= x | hr.gen] * ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) + + Pr[= x | hr.gen] * (if ¬ Good x.1 x.2 then 1 else 0) := by + intro x + by_cases hx : x ∈ support hr.gen + · by_cases hg : Good x.1 x.2 + · have := mul_le_mul' (le_refl (Pr[= x | hr.gen])) (hperkey x hx hg) + calc Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts x.1 x.2) x.1] + ≤ Pr[= x | hr.gen] * + (Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1] + + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk)) := this + _ = Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1] + + Pr[= x | hr.gen] * ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) := + mul_add .. + _ ≤ _ := by simp [hg] + · -- Off `Good`: real ≤ 1, charged to the indicator slot. + have : Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts x.1 x.2) x.1] ≤ + Pr[= x | hr.gen] * (if ¬ Good x.1 x.2 then 1 else 0) := by + simp only [hg, not_false_eq_true, if_true] + exact mul_le_mul' le_rfl probOutput_le_one + exact le_trans this le_add_self + · simp [probOutput_eq_zero_of_not_mem_support hx] + calc ∑' x, Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts x.1 x.2) x.1] + ≤ ∑' x, (Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1] + + Pr[= x | hr.gen] * ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) + + Pr[= x | hr.gen] * (if ¬ Good x.1 x.2 then 1 else 0)) := + ENNReal.tsum_le_tsum hpt + _ = (∑' x, Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1]) + + (∑' x, Pr[= x | hr.gen] * ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk)) + + (∑' x, Pr[= x | hr.gen] * (if ¬ Good x.1 x.2 then 1 else 0)) := by + rw [ENNReal.tsum_add, ENNReal.tsum_add] + _ ≤ (∑' x, Pr[= x | hr.gen] * Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim x.1 x.2) x.1]) + + ENNReal.ofReal (perKeyLoss qS qH ε p_abort ζ_zk) + ENNReal.ofReal δ := by + gcongr + · rw [ENNReal.tsum_mul_right, tsum_probOutput_of_liftM_PMF, one_mul] + · calc ∑' x, Pr[= x | hr.gen] * (if ¬ Good x.1 x.2 then 1 else 0) + = ∑' x, if ¬ Good x.1 x.2 then Pr[= x | hr.gen] else 0 := by + refine tsum_congr fun x => ?_; by_cases hg : Good x.1 x.2 <;> simp [hg] + _ = Pr[fun xw : Stmt × Wit => ¬ Good xw.1 xw.2 | hr.gen] := by + rw [probEvent_eq_tsum_ite] + _ ≤ ENNReal.ofReal δ := hGood + -- Final: glue with the NMA bridge and reassociate the loss. + refine le_trans hbound ?_ + rw [cmaToNmaLoss_eq_perKeyLoss_add, ENNReal.ofReal_add hPK hδ, add_assoc] + gcongr + exact probOutput_hybridExp_sim_le_managedRoNmaExp ids hr M maxAttempts sim adv + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- Cache-invariant companion to `simulatedNmaAdv`: the reduction issues at most `qH` +live hash queries (the signing simulation samples transcripts using only uniform +queries and programs the managed cache). Mirrors +`FiatShamir.simulatedNmaAdv_hashQueryBound` from the Σ-protocol track. -/ +lemma simulatedNmaAdv_nmaHashQueryBound + [Finite Chal] [Inhabited Chal] + (qS qH : ℕ) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) : + ∀ pk, FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (simulatedNmaAdv ids hr M maxAttempts sim adv).main pk) qH := by + haveI : Fintype Chal := Fintype.ofFinite Chal + letI : IsUniformSpec ((M × Commit →ₒ Chal) : OracleSpec _) := + IsUniformSpec.ofFintypeInhabited _ + intro pk + let spec := unifSpec + (M × Commit →ₒ Chal) + let fwd : QueryImpl spec (StateT spec.QueryCache (OracleComp spec)) := + (HasQuery.toQueryImpl (spec := spec) (m := OracleComp spec)).liftTarget _ + let unifSim : QueryImpl unifSpec (StateT spec.QueryCache (OracleComp spec)) := + fun n => fwd (.inl n) + let roSim : QueryImpl (M × Commit →ₒ Chal) + (StateT spec.QueryCache (OracleComp spec)) := fun mc => do + let cache ← get + match cache (.inr mc) with + | some v => pure v + | none => do + let v ← fwd (.inr mc) + modifyGet fun cache => (v, cache.cacheQuery (.inr mc) v) + let sigSim : QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT spec.QueryCache (OracleComp spec)) := fun msg => do + let r ← simulateQ unifSim (firstSome (sim pk) maxAttempts) + match r with + | some (w, c, z) => + modifyGet fun cache => (some (w, z), cache.cacheQuery (.inr (msg, w)) c) + | none => pure none + -- Step bound for `fwd`: 0 live hash queries on `.inl`, exactly 1 on `.inr`. + have hfwd : + ∀ (t : spec.Domain) (s : spec.QueryCache), + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (fwd t).run s) (match t with + | .inl _ => 0 + | .inr _ => 1) := by + intro t s + cases t with + | inl n => + simpa [fwd, QueryImpl.liftTarget_apply, HasQuery.toQueryImpl_apply, + OracleComp.liftM_run_StateT] using + (FiatShamir.nmaHashQueryBound_bind (M := M) (Commit := Commit) (Chal := Chal) + (show FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := liftM (spec.query (.inl n))) 0 by + exact (FiatShamir.nmaHashQueryBound_query_iff (M := M) (Commit := Commit) + (Chal := Chal) (.inl n) 0).2 trivial) + (fun u => + show FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := pure (u, s)) 0 by + trivial)) + | inr mc => + simpa [fwd, QueryImpl.liftTarget_apply, HasQuery.toQueryImpl_apply, + OracleComp.liftM_run_StateT] using + (FiatShamir.nmaHashQueryBound_bind (M := M) (Commit := Commit) (Chal := Chal) + (show FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := liftM (spec.query (.inr mc))) 1 by + exact (FiatShamir.nmaHashQueryBound_query_iff (M := M) (Commit := Commit) + (Chal := Chal) (.inr mc) 1).2 (Nat.succ_pos 0)) + (fun u => + show FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := pure (u, s)) 0 by + trivial)) + -- Step bound for `roSim`: a cache hit issues no live query, a miss issues exactly one. + have hro : + ∀ (mc : M × Commit) (s : spec.QueryCache), + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (roSim mc).run s) 1 := by + intro mc s + cases hs : s (.inr mc) with + | some v => + simp [roSim, hs, FiatShamir.nmaHashQueryBound] + | none => + simp only [FiatShamir.nmaHashQueryBound, Sum.forall, Prod.forall, StateT.run_bind, + StateT.run_get, pure_bind, hs, StateT.run_modifyGet, bind_pure_comp, + isQueryBoundP_map_iff, roSim] at ⊢ hfwd + exact hfwd.2 mc.1 mc.2 s + -- Step bound for `sigSim`: the simulator loop samples under `unifSim` (uniform-only) + -- and then programs the managed cache, issuing no live hash query. + have hsig : + ∀ (msg : M) (s : spec.QueryCache), + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (sigSim msg).run s) 0 := by + intro msg s + have htranscript : + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (simulateQ unifSim (firstSome (sim pk) maxAttempts)).run s) 0 := by + unfold FiatShamir.nmaHashQueryBound + refine OracleComp.IsQueryBoundP.simulateQ_run_of_step + (p := fun _ : ℕ => False) (impl := unifSim) + (oa := firstSome (sim pk) maxAttempts) + (OracleComp.isQueryBoundP_false _ _) + (fun _ h _ => h.elim) + ?_ s + intro n _ s' + have h := hfwd (.inl n) s' + unfold FiatShamir.nmaHashQueryBound at h + exact h + have hcont : ∀ (rs : Option (Commit × Chal × Resp) × spec.QueryCache), + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := StateT.run + (match rs.1 with + | some (w, c, z) => modifyGet fun cache => + (some (w, z), cache.cacheQuery (.inr (msg, w)) c) + | none => + (pure none : StateT spec.QueryCache (OracleComp spec) + (Option (Commit × Resp)))) rs.2) 0 := by + rintro ⟨(_ | ⟨w, c, z⟩), cache⟩ <;> + simp [FiatShamir.nmaHashQueryBound, StateT.run_modifyGet] + have hbind := FiatShamir.nmaHashQueryBound_bind (M := M) (Commit := Commit) + (Chal := Chal) htranscript (fun rs => hcont rs) + simpa [sigSim, StateT.run_bind] using hbind + -- The run-level managed simulation issues at most `qH` live hash queries; the final + -- pure post-processing (erasing the forgery's own verification point from the returned + -- cache, Option B) issues none, so the total bound is `qH + 0 = qH`. + have hrun : FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (simulateQ ((unifSim + roSim) + sigSim) (adv.main pk)).run ∅) qH := by + unfold FiatShamir.nmaHashQueryBound + refine OracleComp.IsQueryBoundP.simulateQ_run_of_step (hQ pk).2 ?_ ?_ ∅ + · rintro ((n | mc) | msg) hp s' + · simp at hp + · exact hro mc s' + · simp at hp + · rintro ((n | mc) | msg) hnp s' + · have h := hfwd (.inl n) s' + unfold FiatShamir.nmaHashQueryBound at h + exact h + · simp at hnp + · exact hsig msg s' + have hpost : ∀ result : (M × Option (Commit × Resp)) × spec.QueryCache, + FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (pure ((result.1.1, result.1.2), + match result.1.2 with + | some (w', _) => Function.update result.2 (Sum.inr (result.1.1, w')) none + | none => result.2) : + OracleComp spec ((M × Option (Commit × Resp)) × spec.QueryCache))) 0 := by + intro result + simp [FiatShamir.nmaHashQueryBound] + have hbind := FiatShamir.nmaHashQueryBound_bind (M := M) (Commit := Commit) + (Chal := Chal) hrun (fun result => hpost result) + change FiatShamir.nmaHashQueryBound (M := M) (Commit := Commit) (Chal := Chal) + (oa := (simulateQ ((unifSim + roSim) + sigSim) (adv.main pk)).run ∅ >>= fun result => + pure ((result.1.1, result.1.2), + match result.1.2 with + | some (w', _) => Function.update result.2 (Sum.inr (result.1.1, w')) none + | none => result.2)) qH + exact hbind + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/BodyHops.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/BodyHops.lean new file mode 100644 index 000000000..82d8d81ca --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/BodyHops.lean @@ -0,0 +1,189 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.Loss + +/-! +# EUF-CMA for Fiat-Shamir with aborts: BodyHops + +The per-signing-query core of the Trans → Sim hop +(`tvDist_run_transSignBody_simSignBody_le`) and the verification-and-freshness +continuation of the hybrid experiment (`hybridVerifyCont`). + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +omit [SampleableType Stmt] in +/-- **Per-signing-query core of the Trans → Sim hop.** From any shared starting cache, +the accepted-only-reprogramming body and the simulated body are within total-variation +distance `ζ_zk · (1 + q + ⋯ + q^(maxAttempts-1)) ≤ ζ_zk / (1 - q)` on their joint +output-and-cache distribution, where `ζ_zk` bounds the per-attempt HVZK error and `q` +the simulator's per-attempt abort probability. + +The cache programming is the same deterministic continuation on both sides +(`signProgramCont`), so the bound reduces to `tvDist_firstSome_le_geometric` on the +private restart loops. -/ +lemma tvDist_run_transSignBody_simSignBody_le + (pk : Stmt) (sk : Wit) (hrel : rel pk sk = true) (msg : M) + {ζ_zk : ℝ} (hhvzk : ids.HVZK sim ζ_zk) + {q : ℝ} (hq : Pr[= none | sim pk].toReal ≤ q) (hq0 : 0 ≤ q) + (s : (M × Commit →ₒ Chal).QueryCache) : + tvDist (StateT.run (transSignBody ids M maxAttempts pk sk msg) s) + (StateT.run (simSignBody M maxAttempts sim pk sk msg) s) ≤ + ζ_zk * ∑ j ∈ Finset.range maxAttempts, q ^ j := by + have hcore : tvDist (firstSome (ids.honestExecution pk sk) maxAttempts) + (firstSome (sim pk) maxAttempts) ≤ + ζ_zk * ∑ j ∈ Finset.range maxAttempts, q ^ j := + tvDist_firstSome_le_geometric (ids.honestExecution pk sk) (sim pk) + (hhvzk pk sk hrel) hq hq0 maxAttempts + have hrw : ∀ (loop : ProbComp (Option (Commit × Chal × Resp))), + StateT.run (liftM loop >>= signProgramCont M msg) s = + loop >>= fun r => StateT.run (signProgramCont M msg r) s := by + intro loop + simp [StateT.run_bind] + rw [transSignBody, simSignBody, hrw, hrw] + exact le_trans (tvDist_bind_right_le _ _ _) hcore + +/-- The hybrid unforgeability experiment at a fixed key pair: run the adversary with the +base handlers and the given signing body, then verify the forgery under the final cache +and apply the freshness check. Instantiating `signBody` with `realSignBody`, +`progSignBody`, `transSignBody`, and `simSignBody` yields the games G₀ — G₃ of the +CMA-to-NMA hybrid chain. -/ +noncomputable def hybridExpAtKey + (signBody : M → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp))) + (pk : Stmt) : ProbComp Bool := do + let ((msg, σ), (cache, signed)) ← StateT.run + (simulateQ + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + hybridSignImpl M signBody) + (adv.main pk)) (∅, []) + let ok ← StateT.run' + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + ((FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).verify pk msg σ)) cache + pure (decide (msg ∉ signed) && ok) + +/-! ## Verification tail -/ + +/-- Verification-and-freshness continuation of `hybridExpAtKey`, as a function of the +adversary's forgery and the final hybrid state. -/ +noncomputable def hybridVerifyCont (pk : Stmt) + (z : (M × Option (Commit × Resp)) × ((M × Commit →ₒ Chal).QueryCache × List M)) : + ProbComp Bool := do + let ok ← StateT.run' + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + ((FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).verify pk z.1.1 z.1.2)) z.2.1 + pure (decide (z.1.1 ∉ z.2.2) && ok) + +omit [SampleableType Stmt] in +lemma hybridExpAtKey_eq_run_bind + (signBody : M → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp))) + (pk : Stmt) : + hybridExpAtKey ids hr M maxAttempts adv signBody pk = + (simulateQ + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + hybridSignImpl M signBody) + (adv.main pk)).run (∅, []) >>= + hybridVerifyCont ids hr M maxAttempts pk := by + refine bind_congr fun z => ?_ + rcases z with ⟨⟨msg, σ⟩, cache, signed⟩ + rfl + +omit [SampleableType Stmt] in +/-- The verification continuation only reads the cache at the forged message's points, +so it is insensitive to cache changes away from them. -/ +lemma hybridVerifyCont_cache_congr (pk : Stmt) (ms : M × Option (Commit × Resp)) + (c₁ c₂ : (M × Commit →ₒ Chal).QueryCache) (l : List M) + (h : ∀ w : Commit, c₁ (ms.1, w) = c₂ (ms.1, w)) : + hybridVerifyCont ids hr M maxAttempts pk (ms, (c₁, l)) = + hybridVerifyCont ids hr M maxAttempts pk (ms, (c₂, l)) := by + rcases ms with ⟨msg, _ | ⟨w, zr⟩⟩ + · rfl + · refine congrArg (· >>= fun ok => pure (decide (msg ∉ l) && ok)) ?_ + have hside : ∀ c : (M × Commit →ₒ Chal).QueryCache, + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + ((FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).verify pk msg (some (w, zr)))).run' c = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + ids.verify pk w cu.1 zr) <$> roStep M c (msg, w) := by + intro c + simp only [FiatShamirWithAbort, simulateQ_bind, roSim.simulateQ_HasQuery_query, + simulateQ_pure] + change Prod.fst <$> (((randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) (msg, w) >>= + fun cc => pure (ids.verify pk w cc zr)).run c) = _ + rw [StateT.run_bind] + rw [show ((randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) (msg, w)).run c = + roStep M c (msg, w) from randomOracle_run_eq_roStep M c (msg, w)] + simp + rw [hside c₁, hside c₂] + cases hc : c₁ (msg, w) with + | some v => + rw [roStep_of_some M hc, + roStep_of_some M (show c₂ (msg, w) = some v from (h w).symm.trans hc)] + simp + | none => + rw [roStep_of_none M hc, + roStep_of_none M (show c₂ (msg, w) = none from (h w).symm.trans hc)] + simp + +omit [SampleableType Stmt] in +/-- When the forged message has already been signed, the freshness conjunct forces the +game output to `false`, so the success probability vanishes regardless of the cache. -/ +lemma probOutput_true_hybridVerifyCont_of_mem (pk : Stmt) + (ms : M × Option (Commit × Resp)) + (c : (M × Commit →ₒ Chal).QueryCache) (l : List M) (hmem : ms.1 ∈ l) : + Pr[= true | hybridVerifyCont ids hr M maxAttempts pk (ms, (c, l))] = 0 := by + rw [hybridVerifyCont, probOutput_bind_eq_tsum] + refine ENNReal.tsum_eq_zero.mpr fun ok => ?_ + rw [probOutput_pure, if_neg (by simp [hmem]), mul_zero] + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/CouplingEngine.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/CouplingEngine.lean new file mode 100644 index 000000000..505c473cd --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/CouplingEngine.lean @@ -0,0 +1,1898 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.HiddenReadFold + +/-! +# EUF-CMA for Fiat-Shamir with aborts: CouplingEngine + +`avgBadM_eager_le_lazy_joint`, a reusable free-monad telescoping engine for +dominating averaged bad masses under a two-measure coupling invariant, together +with its deferral primitives and the attempt-count law. Not on the live path of +the ghost-read bound; retained as general infrastructure. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ## Measure-level eager↔lazy coupling engine + +`avgBadM_eager_le_lazy_joint` is a reusable free-monad telescoping engine for dominating +averaged bad masses under a two-measure coupling invariant. It is not on the live path of +the ghost-read bound, which goes through the ghost-blind first-moment route +(`probEvent_ghostHybridImpl_bad_le_ghostBlind` into `probEvent_ghostBlindImpl_bad_le`). It +is retained here as general infrastructure for a joint-law approach to the eager↔lazy +comparison. + +The engine carries a **two-measure coupling invariant** `Inv νe νl` through the free-monad +induction on `oa`: the uniform and signing steps preserve `Inv` on the per-output post-step +measures (the handlers are definitionally identical on those steps, so `postStepOutM` agrees +and `Inv` is threaded unchanged), the pure leaf compares the carried bad mass under `Inv`, and +the read step supplies the genuine deferred-sampling inequality — the eager read's averaged +ghost-hit marginal over `νe` dominated by the lazy read's deferred-fire marginal over `νl`. + +A per-state (single-measure, `νe = νl`) version of the read inequality is **false** — at a +committed ghost-hit state the eager read flips the bad flag with mass `1` while the lazy read +fires with sub-unit mass — so the two-measure coupling is essential for any future application. -/ + +omit [SampleableType Stmt] in +/-- **Two-measure eager↔lazy averaged-bad coupling engine.** Threads a coupling invariant +`Inv : (state-measure) → (state-measure) → Prop` through the free-monad induction on `oa`: + +* `h_step_eq`: a non-read step (uniform forward or signing query) preserves `Inv` on the + per-output post-step measures. The eager and lazy handlers are definitionally identical on + these steps, so the two `postStepOutM` measures are produced by the same map and `Inv` is + threaded across them. +* `h_pure`: at a pure leaf the carried bad mass of `νe` is dominated by that of `νl` (under + `Inv`). +* `h_read`: at a random-oracle read step, the eager read's averaged ghost-hit bad marginal + over `νe` is dominated by the lazy read's deferred-fire marginal over `νl` (under `Inv`), + with the invariant-conditional inductive hypothesis on the continuations available. + +Given these, `avgBadM eager νe oa ≤ avgBadM lazy νl oa` for every `Inv`-related pair. This is +the measure-level coupling vehicle: the read-step averaging (signing-time draw into `νe` +versus read-time redraw of `νl`) is exactly what the per-output post-step *measures* (not +per-state Diracs) carry, which is why a per-state comparison cannot replace it. -/ +lemma avgBadM_eager_le_lazy_joint (pk : Stmt) (sk : Wit) + (Inv : (GhostState M Commit Chal → ℝ≥0∞) → (GhostState M Commit Chal → ℝ≥0∞) → Prop) + (h_step_eq : ∀ (νe νl : GhostState M Commit Chal → ℝ≥0∞), Inv νe νl → + ∀ (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain), + (¬ t matches Sum.inl (Sum.inr _)) → + ∀ u, Inv (OracleComp.ProgramLogic.Relational.postStepOutM + (ghostHybridImpl ids M maxAttempts true pk sk) νe t u) + (OracleComp.ProgramLogic.Relational.postStepOutM + (lazyGhostHybridImpl ids M maxAttempts pk sk) νl t u)) + (h_read : ∀ (νe νl : GhostState M Commit Chal → ℝ≥0∞), Inv νe νl → + ∀ (mc : M × Commit) + (cont : Chal → OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) (M × Option (Commit × Resp))), + (∀ u νe' νl', Inv νe' νl' → + OracleComp.ProgramLogic.Relational.avgBadM + (ghostHybridImpl ids M maxAttempts true pk sk) νe' (cont u) + ≤ OracleComp.ProgramLogic.Relational.avgBadM + (lazyGhostHybridImpl ids M maxAttempts pk sk) νl' (cont u)) → + (∑' p : GhostState M Commit Chal, νe p * + ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk + (Sum.inl (Sum.inr mc))).run p] * + Pr[ fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => + w.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (cont z.1)).run z.2]) + ≤ ∑' p : GhostState M Commit Chal, νl p * + ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (lazyGhostHybridImpl ids M maxAttempts pk sk + (Sum.inl (Sum.inr mc))).run p] * + Pr[ fun w : (M × Option (Commit × Resp)) × GhostState M Commit Chal => + w.2.2 = true | + (simulateQ (lazyGhostHybridImpl ids M maxAttempts pk sk) (cont z.1)).run z.2]) + (h_pure : ∀ (νe νl : GhostState M Commit Chal → ℝ≥0∞), Inv νe νl → + ∀ _x : M × Option (Commit × Resp), + (∑' p : GhostState M Commit Chal, νe p * (if p.2 = true then 1 else 0)) + ≤ ∑' p : GhostState M Commit Chal, νl p * (if p.2 = true then 1 else 0)) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (M × Option (Commit × Resp))) : + ∀ νe νl : GhostState M Commit Chal → ℝ≥0∞, Inv νe νl → + OracleComp.ProgramLogic.Relational.avgBadM + (ghostHybridImpl ids M maxAttempts true pk sk) νe oa + ≤ OracleComp.ProgramLogic.Relational.avgBadM + (lazyGhostHybridImpl ids M maxAttempts pk sk) νl oa := by + induction oa using OracleComp.inductionOn with + | pure x => + intro νe νl hInv + rw [OracleComp.ProgramLogic.Relational.avgBadM_pure, + OracleComp.ProgramLogic.Relational.avgBadM_pure] + exact h_pure νe νl hInv x + | @query_bind t cont ih => + intro νe νl hInv + rcases t with (n | mc) | msg + · rw [OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq_tsum_output, + OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq_tsum_output] + refine ENNReal.tsum_le_tsum fun u => ?_ + exact ih u _ _ (h_step_eq νe νl hInv (Sum.inl (Sum.inl n)) (by simp) u) + · rw [OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq, + OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq] + exact h_read νe νl hInv mc cont (fun u νe' νl' h => ih u νe' νl' h) + · rw [OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq_tsum_output, + OracleComp.ProgramLogic.Relational.avgBadM_query_bind_eq_tsum_output] + refine ENNReal.tsum_le_tsum fun u => ?_ + exact ih u _ _ (h_step_eq νe νl hInv (Sum.inr msg) (by simp) u) + +omit [SampleableType Stmt] in +/-- **M1: the identical-until-bad / ghost-blind reduction** (foundational step of the +ghost-read bound). The eager hybrid handler `ghostHybridImpl … true` and the ghost-blind +handler `ghostBlindImpl` flip the adversarial-read bad flag with *exactly the same* +probability at the empty-cache Dirac start: + +`Pr[bad | (simulateQ (ghostHybridImpl … true) (adv.main pk)).run δ_∅]` +` = Pr[bad | (simulateQ ghostBlindImpl (adv.main pk)).run δ_∅]`. + +The two handlers are *identical until bad*: they coincide on uniform queries, on signing +queries, and on ghost-*miss* reads (all run the same `roStep` / `ghostSignBody`), and on a +ghost-*hit* read both flip the bad flag (`ghostBlindImpl_agree_good`), while neither ever +unsets it (`ghostHybridImpl_bad_mono` / `ghostBlindImpl_bad_mono`). The blind handler answers +a hit from the real layer instead of returning the ghost value, so the ghost-key values never +influence the run — they are consulted only to record the would-hit. Because the runs differ +only on the already-bad trajectory (where both flags read `true`), the bad marginals coincide, +by the exact identical-until-bad bad-event equality +`probEvent_output_bad_eq'`. -/ +lemma probEvent_ghostHybridImpl_bad_eq_ghostBlind (pk : Stmt) (sk : Wit) : + Pr[ fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + = Pr[ fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] := + OracleComp.ProgramLogic.Relational.probEvent_output_bad_eq' + (ghostHybridImpl ids M maxAttempts true pk sk) + (ghostBlindImpl ids M maxAttempts pk sk) + (ghostBlindImpl_agree_good ids M maxAttempts pk sk) + (ghostHybridImpl_bad_mono ids M maxAttempts true pk sk) + (ghostBlindImpl_bad_mono ids M maxAttempts pk sk) + (adv.main pk) (((∅, ∅), []) : _) + +omit [SampleableType Stmt] in +/-- **M1 (≤ form).** The eager ghost-read bad mass is bounded by the ghost-blind handler's +bad mass at the empty-cache Dirac start; immediate from the equality +`probEvent_ghostHybridImpl_bad_eq_ghostBlind`. This is the reduction the read-bound spine +chains with M2 (reads ⊥ ghost-key values) and M3 (geometric first-fire charge). -/ +lemma probEvent_ghostHybridImpl_bad_le_ghostBlind (pk : Stmt) (sk : Wit) : + Pr[ fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ Pr[ fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] := + (probEvent_ghostHybridImpl_bad_eq_ghostBlind ids hr M maxAttempts adv pk sk).le + +/-! ### M3: the geometric first-fire charge (assembly) + +`probEvent_ghostBlind_bad_le_of_fac` is the **M3** charge: given the **M2** deferred-sampling +factorization `hfac` — the ghost-blind run's bad marginal exhibited as the value-free +multi-key hidden-target game `kn >>= hiddenReadList (Prod.fst <$> ids.commit pk sk) (qH+1) σ` +(rejected commitment values deferred to a front block, read off by the adversary's adaptive +all-miss strategy `σ`) — the target bound `qS·(qH+1)·ε/(1-p)` follows by the +union-bound + geometric-fold pipeline: + +* per-target guessing bound `hGuess` (raw `Pr[= w | commit] ≤ ε`) feeds the multi-key + first-fire union bound `OracleComp.probEvent_bind_hiddenReadList_le`, giving + `E[n]·((qH+1)·ε)` where `E[n] = ∑' n, Pr[= n | kn]·n` is the expected ghost-key count; +* the expected-count mean bound `hmean` (`E[n] ≤ qS/(1-p)`, the aggregate of + `tsum_probOutput_commit_mul_abort_le` over the `qS` signing queries) folds into the target + via `hiddenReadList_fold_le_target`. + +The `Pr[reject|mc] ≤ 1` skew-drop is already baked into the raw-`commit` per-target bound +`hGuess` (the hidden targets are drawn from the *raw* commit law, not the rejection-conditioned +law), so no skew survives into this charge. The accepting attempt contributes `0` because it +is not a rejected draw and so is absent from `kn`'s key count. -/ +omit [SampleableType Stmt] in +theorem probEvent_ghostBlind_bad_le_of_fac + (qS qH : ℕ) (ε p_abort : ℝ) (hp : p_abort < 1) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (σ : List Bool → Commit) (kn : ProbComp ℕ) + (hmean : ∑' n : ℕ, Pr[= n | kn] * (n : ℝ≥0∞) + ≤ ENNReal.ofReal ((qS : ℝ) / (1 - p_abort))) + (hfac : Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ Pr[(fun b : Bool => b = true) | + kn >>= fun n => OracleComp.hiddenReadList (Prod.fst <$> ids.commit pk sk) (qH + 1) σ n]) : + Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ ENNReal.ofReal (qS * ((qH : ℝ) + 1) * ε / (1 - p_abort)) := by + refine (OracleComp.probEvent_le_of_eq_bind_hiddenReadList (oa := Prod.fst <$> ids.commit pk sk) + (ε := ENNReal.ofReal ε) hGuess (qH + 1) σ kn hfac).trans ?_ + -- The averaged union bound `E[n]·((qH+1)·ε)` folds into the target via the geometric fold. + refine le_trans (le_of_eq ?_) + (hiddenReadList_fold_le_target qS qH ε p_abort hp (fun n => Pr[= n | kn]) hmean) + -- Reconcile the `((qH+1)·ofReal ε)` factor shapes: `((qH:ℝ≥0∞)+1)` vs `↑(qH+1)`. + rw [← ENNReal.tsum_mul_right] + refine tsum_congr fun n => ?_ + rw [mul_assoc] + congr 2 + push_cast + ring + +omit [SampleableType Stmt] in +/-- **Ghost-blind read-step bad indicator** (an M2 structural building block). Starting from a +state with the bad flag unset, the ghost-blind handler's adversarial random-oracle read at `mc` +sets the bad flag with mass exactly `1` if `mc` lies in the ghost-cache domain and `0` +otherwise. Identical indicator to the eager handler's `probOutput_ghostHybridImpl_read_bad`, but +here the *answer* is `roStep` on the real layer in **both** branches (hit and miss): the ghost +value never reaches the output, only the bad flag records the structural hit. This is the +manifest output-irrelevance of `ghostBlindImpl` at the read step — the per-read membership test +the M2 factorization reads off as a `hiddenReadList` probe. -/ +lemma probEvent_ghostBlindImpl_read_bad (pk : Stmt) (sk : Wit) (mc : M × Commit) + (s : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) : + Pr[fun z : Chal × GhostState M Commit Chal => z.2.2 = true | + (ghostBlindImpl ids M maxAttempts pk sk (.inl (.inr mc))).run (s, false)] = + if s.1.2 mc = none then 0 else 1 := by + rw [ghostBlindImpl_eq_ghostHybridImpl_false] + cases hgh : s.1.2 mc with + | some v => + rw [ghostHybridImpl_run_ro_ghost_some ids M maxAttempts false pk sk hgh] + simp + | none => + rw [ghostHybridImpl_run_ro_ghost_none ids M maxAttempts false pk sk hgh, if_pos rfl] + simp [probEvent_eq_zero] + +/-! ### Stage 2: single-query deferral primitives + +The value-free foundation (`blindStepProj_map_ghostBlindImpl_indep`, Stage 1) shows the stored +ghost commitment values never feed back into the ghost-blind run. The two lemmas here are the +*single-query* deferral atoms that Stage 3 instantiates once per rejected signing attempt: + +* `ghostBlindImpl_read_singletonGhost_bad` connects the ghost-blind read handler to the + membership predicate. With the ghost cache holding a single rejected-attempt key `(msg, w) ↦ c`, + an adversarial read at `mc` fires the bad flag *exactly* when `mc = (msg, w)` — the structural + read-hit test that `OracleComp.readMany` models for one hidden target. +* `ghostBlind_singleDraw_fire_le` is the commit-sampler instance of the deferral primitive + `OracleComp.probEvent_bind_fire_le_of_gen`: a run that draws one ghost commitment up front and + feeds it *only* through the fixed `q`-read game of a value-free generator fires with probability + at most `q · ε`. This is the "front-loaded one draw" charge; Stage 3 supplies the value-free + generator `gen` from `blindStepProj_map_ghostBlindImpl_indep` and folds the `qS` per-query + charges into the aggregate `kn >>= drawList` block. -/ + +omit [SampleableType Stmt] in +/-- **Stage 2 read-membership atom.** With the ghost cache holding exactly the single +rejected-attempt key `(msg, w) ↦ c`, an adversarial random-oracle read at `mc` in the ghost-blind +run fires the bad flag with mass `1` when `mc = (msg, w)` and `0` otherwise. This is the structural +single-target read-hit indicator (`OracleComp.readMany`'s per-read test) realised by the +ghost-blind handler: the value `w` enters the run *only* through this membership test, never through +the read's answer (which is `roStep` on the real layer — `probEvent_ghostBlindImpl_read_bad`). -/ +lemma ghostBlindImpl_read_singletonGhost_bad (pk : Stmt) (sk : Wit) (mc : M × Commit) (msg : M) + (w : Commit) (c : Chal) (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + Pr[fun z : Chal × GhostState M Commit Chal => z.2.2 = true | + (ghostBlindImpl ids M maxAttempts pk sk (.inl (.inr mc))).run + (((re, (∅ : (M × Commit →ₒ Chal).QueryCache).cacheQuery (msg, w) c), l), false)] = + if mc = (msg, w) then 1 else 0 := by + rw [probEvent_ghostBlindImpl_read_bad ids M maxAttempts pk sk mc + ((re, (∅ : (M × Commit →ₒ Chal).QueryCache).cacheQuery (msg, w) c), l)] + by_cases h : mc = (msg, w) + · subst h + rw [if_neg (by simp), if_pos rfl] + · rw [if_pos (by simp [QueryCache.cacheQuery_of_ne _ _ h]), if_neg h] + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- **Stage 2 single-query deferral.** A run that draws one ghost commitment +`w ← Prod.fst <$> ids.commit pk sk` (each outcome of mass at most `ε`) and feeds it to a +*value-free* continuation `k w = gen >>= fun p => pure (p.1, readMany w q p.2)` — a `w`-free +generator `gen` producing the visible output `p.1` and the `q`-read strategy `p.2`, with the drawn +commitment entering *only* through the fixed read game `readMany w q p.2` — fires with probability +at most `q · ε`. + +This is the commit-sampler instance of `OracleComp.probEvent_bind_fire_le_of_gen`. The hypothesis +`hk` is exactly the value-freeness supplied by `blindStepProj_map_ghostBlindImpl_indep` (Stage 1): +because the ghost value never influences the run, the continuation's fire-marginal factors through +a `w`-free generator with the draw confined to the read-membership test +(`ghostBlindImpl_read_singletonGhost_bad`). Stage 3 instantiates this once per rejected attempt. -/ +lemma ghostBlind_singleDraw_fire_le {α : Type} (pk : Stmt) (sk : Wit) {ε : ℝ≥0∞} + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ε) + (q : ℕ) (gen : ProbComp (α × (List Bool → Commit))) (k : Commit → ProbComp (α × Bool)) + (hk : ∀ w : Commit, k w = gen >>= fun p => pure (p.1, OracleComp.readMany w q p.2)) : + Pr[(fun z : α × Bool => z.2 = true) | (Prod.fst <$> ids.commit pk sk) >>= k] + ≤ (q : ℝ≥0∞) * ε := + OracleComp.probEvent_bind_fire_le_of_gen hGuess q gen k hk + +/-! ### M2: the deferred-sampling factorization + +The **M2** content is the ghost-blind run's bad marginal *factoring* as a value-free deferred-draw +game. In this module it appears as the hypothesis `hfac` of `probEvent_ghostBlind_bad_le_of_fac`, +which is the σ-indexed (front-loaded hidden-target) form of the factorization. The headline takes +the σ-free route instead, charging the ghost-read bound through the first-moment residual +`readRecord_expected_coincidences_le` (the expected coincidence count of the value-free recorded +read-commit list with the recorded rejected draws). + +Why it factors (the sound argument). In `ghostBlindImpl` an adversarial random-oracle read at a +ghost-cache hit answers from the *real* layer via `roStep` — identically to a miss — and only +*records* the would-hit by flipping the bad flag (`ghostBlindImpl_eq_ghostHybridImpl_false`, +`ghostBlindImpl_agree_good`). So the ghost-cache *values* are write-only side-data: they never +influence the run's outputs or continuation. Consequently the run's joint law of (adversary read +points, reject pattern / loop lengths, real cache) is produced by a value-free run that is +*independent of the stored commitment values*; those values are drawn `~ Prod.fst <$> ids.commit` +per *rejected* attempt and gated into the ghost cache by the reject decision. + +Formalize as a deferred-sampling factorization: pull every rejected attempt's commitment draw +into the recorded drawn-list of the deferred handler `deferredDrawReadImpl`, independent of the +value-free recorded read-commit list. The expected coincidence count is then bounded by +`(#reads) · (#draws) · (max draw mass) ≤ (qH+1) · ε · E[#attempts]`, with `E[#attempts] ≤ +qS/(1-p)` the aggregate of `tsum_probOutput_commit_mul_abort_le` over the `qS` signing queries +(each rejected attempt is reached with geometric probability, summed by `geomAttemptSum_le`). + +Supporting tools for the σ-indexed form: the read-marginal equalities +`probEvent_ghostHybridImpl_read_bad_single_eq_lazyFire` / +`probOutput_eagerMultiReadBad_eq_lazyFire_or` (the signing-time→read-time draw commutation, here +applied to the value-free `ghostBlindImpl` continuation rather than the eager one whose +continuation depends on the read value), `probOutput_lazyGhostFire_one`, and the value-free +read-answer agreement (`ghostBlindImpl`'s hit branch is `roStep`, the same `map`-of-`roStep` as a +miss and as the lazy handler). Lifting the output-irrelevance through the `simulateQ` fold, so that +the per-rejected-attempt draws commute to the front independently of the intervening adversary +computation, is carried out on the σ-free route by +`evalDist_deferredDrawRead_eq_drawList_tapeDrawRead` in `Security/TapeFactorization.lean`. + +This factorization route is sound precisely because `ghostBlindImpl` reads never feed the ghost +value into the run, so the draws are genuinely deferrable. -/ + +/-! ### Stage 3a: deferred-handler ingredients + +The sound first-moment route couples the eager ghost-blind run to a *deferred* handler `impl₂` (a +genuine `QueryImpl`) via a per-step coupling relation `Rrun` on the two run distributions and the +two bad predicates `bad₁ / bad₂`. This block constructs those ingredients. + +The deferred handler `deferredDrawImpl` carries, instead of eager-committed ghost keys, the +*accumulated list of drawn rejected-attempt commitments* (the front block, grown lazily as sign +steps draw) together with the real cache, the signed list, and the "some recorded read hit a drawn +commitment" flag. Its state is + + `DeferredState M Commit Chal := + (((M × Commit →ₒ Chal).QueryCache × List M) × List Commit) × Bool`. + +Branch behaviour, designed so that the *observable* component (output, real cache, signed list) +coincides with the ghost-blind handler value-free (Stage 1): +* a **uniform** query forwards exactly as `ghostBlindImpl` does, touching neither the drawn list nor + the bad flag; +* a **random-oracle read** answers from the real layer via `roStep` (identical read point and answer + to `ghostBlindImpl`'s value-free hit/miss branches) and sets the bad flag iff the read point's + commitment `mc.2` is among the accumulated drawn list — the deferred counterpart of the eager + membership test against the ghost domain; +* a **signing** query runs the value-free signing body (`run_ghostSignBody_fst` recovers + `transSignBody`, the accepted-only loop) for the output and real cache, and appends to the drawn + list one i.i.d. raw `Prod.fst <$> ids.commit pk sk` draw per rejected attempt, mirroring the eager + ghost writes. -/ + +/-- State of the deferred-draw handler: real cache, signed-message list, the accumulated list of +drawn rejected-attempt commitments (the deferral front block), and the monotone "some recorded read +hit a drawn commitment" flag. The drawn list replaces the eager ghost cache: where `ghostBlindImpl` +commits sampled keys into its ghost layer, `deferredDrawImpl` only records the *list* of drawn +commitments, which is later read off as the front `drawList` block. -/ +abbrev DeferredState (M Commit Chal : Type) : Type := + (((M × Commit →ₒ Chal).QueryCache × List M) × List Commit) × Bool + +/-- Draw-collecting signing body: mirrors `ghostSignBody` but threads only the *real* cache and +accumulates the list of drawn *rejected*-attempt commitments instead of writing them to a ghost +layer. Returns `(output, drawn commits this query)`. Only the rejected-attempt commitments are +recorded, in attempt order; the accepted attempt (whose commitment is returned to the caller and +cached in the real layer) records nothing, exactly mirroring `ghostSignBody`, whose ghost layer +holds the rejected commitments and `uncacheQuery`-s the accepted one. Forgetting the drawn list +recovers `transSignBody` (the value-free output and real cache), and the drawn list is exactly the +list of i.i.d. raw `Prod.fst <$> ids.commit pk sk` samples taken on the *rejected* attempts — the +value-free side-data that never feeds back into the run's outputs. -/ +noncomputable def ghostSignDrawBody (pk : Stmt) (sk : Wit) (msg : M) : + ℕ → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp) × List Commit) + | 0 => pure (none, []) + | n + 1 => do + let (w, st) ← liftM (ids.commit pk sk) + let c ← (liftM (uniformSample Chal) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp Chal) + let oz ← liftM (ids.respond pk sk st c) + match oz with + | some z => + modify fun cache => cache.cacheQuery (msg, w) c + pure (some (w, z), []) + | none => + let (res, ws) ← ghostSignDrawBody pk sk msg n + pure (res, w :: ws) + +omit [SampleableType Stmt] in +/-- One-step unfolding of the draw-collecting signing body, mirroring `run_ghostSignBody_succ`. +The body draws a commitment `w`, samples a challenge `ch`, responds, and on accept records *no* +drawn commitment (the accepted commit is returned, not deferred) while on reject prepends `w` to +the recursively collected list of rejected commitments. -/ +lemma run_ghostSignDrawBody_succ (pk : Stmt) (sk : Wit) (msg : M) (n : ℕ) + (re : (M × Commit →ₒ Chal).QueryCache) : + (ghostSignDrawBody ids M pk sk msg (n + 1)).run re = + ids.commit pk sk >>= fun ws => + uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re := by + simp only [ghostSignDrawBody, bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, + pure_bind] + refine congrArg (ids.commit pk sk >>= ·) (funext fun ws => ?_) + obtain ⟨w, st⟩ := ws + refine congrArg (uniformSample Chal >>= ·) (funext fun ch => ?_) + refine congrArg (ids.respond pk sk st ch >>= ·) (funext fun oz => ?_) + cases oz with + | some z => simp [StateT.run_modify] + | none => simp [StateT.run_bind, StateT.run_pure, map_eq_bind_pure_comp, Function.comp] + +/-! ### Body-level tape resampling (the per-body half of the tape factorization) + +The draw-collecting signing body `ghostSignDrawBody` draws each attempt's commitment *inline*. The +genuine fold-lift content of the ghost-read bound is to front-load every interleaved per-attempt +draw into one independent block, so the drawn *values* factor away from the value-free adversarial +read points. The body-level half of that program — recasting one signing body's inline draws as +consumption from a *pre-drawn* tape — is proved here as a distributional equality +`evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody`: + +`𝒟[(ghostSignDrawBody … n).run re] = 𝒟[drawList (ids.commit pk sk) n >>= tapeSignBody … tape]`. + +Pre-drawing the `n`-block of full `(Commit × PrvState)` commitment draws and consuming them +head-first (`tapeSignBody`) is distributionally identical to drawing them inline: the control flow +(accept/reject, via the inline `uniformSample`/`respond`) reads the *same* tape values, and the +unused suffix on an early accept is discarded. The proof is a structural induction on `n` that, at +each attempt, commutes the recursive front block `drawList n` past the inline +`uniformSample`/`respond` draws (`evalDist_bind_comm_probComp`, the i.i.d. resampling step) and +matches the reject-branch recursion to the inductive hypothesis. + +This is the local, per-body `bind`-commutation. Its lift across the *opaque adversary* +`simulateQ (oa)` fold — the interleaved per-query draw blocks all commuting to the front, past the +adaptive read points — is `evalDist_deferredDrawRead_eq_drawList_tapeDrawRead` in +`Security/TapeFactorization.lean`. -/ + +/-- **i.i.d. bind-commutation at the distribution level for `ProbComp`.** Two independent draws +`oa`, `ob` feeding a common continuation `k` may be drawn in either order without changing the +output distribution. The `OracleComp` monad is *not* commutative as a free monad (its `bind` is +syntactic), but its `evalDist` image into `SPMF` is: the two iterated sums over the independent +draws exchange by `ENNReal.tsum_comm`. This is the local resampling step that front-loads an +output-irrelevant draw past its continuation. -/ +theorem evalDist_bind_comm_probComp {α β γ : Type} (oa : ProbComp α) (ob : ProbComp β) + (k : α → β → ProbComp γ) : + 𝒟[oa >>= fun a => ob >>= fun b => k a b] = 𝒟[ob >>= fun b => oa >>= fun a => k a b] := by + refine SPMF.ext fun x => ?_ + rw [show 𝒟[oa >>= fun a => ob >>= fun b => k a b] x + = Pr[= x | oa >>= fun a => ob >>= fun b => k a b] from (probOutput_def _ _).symm, + show 𝒟[ob >>= fun b => oa >>= fun a => k a b] x + = Pr[= x | ob >>= fun b => oa >>= fun a => k a b] from (probOutput_def _ _).symm] + rw [probOutput_bind_eq_tsum] + rw [show (∑' a : α, Pr[= a | oa] * Pr[= x | ob >>= fun b => k a b]) + = ∑' (a : α) (b : β), Pr[= a | oa] * (Pr[= b | ob] * Pr[= x | k a b]) from + tsum_congr fun a => by rw [probOutput_bind_eq_tsum, ENNReal.tsum_mul_left]] + rw [probOutput_bind_eq_tsum] + rw [show (∑' b : β, Pr[= b | ob] * Pr[= x | oa >>= fun a => k a b]) + = ∑' (b : β) (a : α), Pr[= b | ob] * (Pr[= a | oa] * Pr[= x | k a b]) from + tsum_congr fun b => by rw [probOutput_bind_eq_tsum, ENNReal.tsum_mul_left]] + rw [ENNReal.tsum_comm] + exact tsum_congr fun a => tsum_congr fun b => by ring + +/-- **Dropping a never-failing prefix at the distribution level.** A leading draw `od` whose +continuation ignores its value contributes only its total mass; when `od` never fails (mass `1`, +e.g. a `drawList` front block) it can be discarded from the output distribution. -/ +theorem evalDist_bind_const_neverFails {α γ : Type} (od : ProbComp α) (hmass : Pr[⊥ | od] = 0) + (k : ProbComp γ) : 𝒟[od >>= fun _ => k] = 𝒟[k] := by + refine SPMF.ext fun x => ?_ + rw [show 𝒟[od >>= fun _ => k] x = Pr[= x | od >>= fun _ => k] from (probOutput_def _ _).symm, + show 𝒟[k] x = Pr[= x | k] from (probOutput_def _ _).symm] + rw [probOutput_bind_const, hmass]; simp + +/-- **Distribution-level congruence under a leading bind.** If two continuations agree as +distributions pointwise then the bound computations agree as distributions. -/ +theorem evalDist_bind_congr_left {α β : Type} (oa : ProbComp α) (f g : α → ProbComp β) + (h : ∀ a, 𝒟[f a] = 𝒟[g a]) : 𝒟[oa >>= f] = 𝒟[oa >>= g] := by + rw [evalDist_bind, evalDist_bind]; exact congrArg _ (funext h) + +/-- **Tape-consuming signing body.** Identical to `ghostSignDrawBody` except that each attempt's +commitment draw `(Commit × PrvState)` is *consumed* from a pre-drawn tape (head-first) instead of +drawn inline. The challenge sampling and response stay inline. On accept the remaining tape suffix +is discarded; an empty tape ends the loop (mirroring budget exhaustion). The recorded +rejected-commit list is built exactly as in `ghostSignDrawBody`. -/ +noncomputable def tapeSignBody (pk : Stmt) (sk : Wit) (msg : M) : + List (Commit × PrvState) → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp) × List Commit) + | [] => pure (none, []) + | (w, st) :: rest => do + let c ← (liftM (uniformSample Chal) : + StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp Chal) + let oz ← liftM (ids.respond pk sk st c) + match oz with + | some z => + modify fun cache => cache.cacheQuery (msg, w) c + pure (some (w, z), []) + | none => + let (res, ws) ← tapeSignBody pk sk msg rest + pure (res, w :: ws) + +omit [SampleableType Stmt] in +/-- One-step unfolding of the tape-consuming signing body on a non-empty tape, mirroring +`run_ghostSignDrawBody_succ`: the head `(w, st)` is consumed, a challenge sampled and a response +computed; on accept the body records no commitment, on reject it prepends `w` to the recursively +collected list and continues on the tape tail. -/ +lemma run_tapeSignBody_cons (pk : Stmt) (sk : Wit) (msg : M) (w : Commit) (st : PrvState) + (rest : List (Commit × PrvState)) (re : (M × Commit →ₒ Chal).QueryCache) : + (tapeSignBody ids M pk sk msg ((w, st) :: rest)).run re = + uniformSample Chal >>= fun ch => + ids.respond pk sk st ch >>= fun oz => + match oz with + | some z => pure ((some (w, z), []), re.cacheQuery (msg, w) ch) + | none => (fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (tapeSignBody ids M pk sk msg rest).run re := by + simp only [tapeSignBody, bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, pure_bind] + refine congrArg (uniformSample Chal >>= ·) (funext fun ch => ?_) + refine congrArg (ids.respond pk sk st ch >>= ·) (funext fun oz => ?_) + cases oz with + | some z => simp [StateT.run_modify] + | none => simp [StateT.run_bind, StateT.run_pure, map_eq_bind_pure_comp, Function.comp] + +omit [SampleableType Stmt] in +/-- **The body-level tape resampling equality.** Drawing one signing body's `n` attempt +commitments inline (`ghostSignDrawBody`) is distributionally identical to pre-drawing the `n`-block +of full commitment draws into a tape and consuming it head-first (`tapeSignBody`): + +`𝒟[(ghostSignDrawBody … n).run re] = 𝒟[drawList (ids.commit pk sk) n >>= tapeSignBody … tape]`. + +The proof inducts on `n`: at each attempt, the recursive front block `drawList n` is commuted past +the inline `uniformSample`/`respond` draws (the i.i.d. resampling step +`evalDist_bind_comm_probComp`), the accepting branch discards the unused suffix +(`evalDist_bind_const_neverFails`, `drawList` never +fails), and the rejecting branch matches the inductive hypothesis. This is the per-body half of the +tape factorization; its lift across the opaque adversary fold is +`evalDist_deferredDrawRead_eq_drawList_tapeDrawRead`. -/ +theorem evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody (pk : Stmt) (sk : Wit) (msg : M) + (n : ℕ) (re : (M × Commit →ₒ Chal).QueryCache) : + 𝒟[(ghostSignDrawBody ids M pk sk msg n).run re] = + 𝒟[OracleComp.drawList (ids.commit pk sk) n >>= fun tape => + (tapeSignBody ids M pk sk msg tape).run re] := by + induction n generalizing re with + | zero => simp [ghostSignDrawBody, tapeSignBody, OracleComp.drawList] + | succ n ih => + rw [run_ghostSignDrawBody_succ, OracleComp.drawList] + simp only [bind_assoc, pure_bind] + rw [evalDist_bind, evalDist_bind] + refine congrArg (𝒟[ids.commit pk sk] >>= ·) (funext fun ws => ?_) + obtain ⟨w, st⟩ := ws + simp only [run_tapeSignBody_cons] + set dl := OracleComp.drawList (ids.commit pk sk) n with hdl + have hdlmass : Pr[⊥ | dl] = 0 := by rw [hdl]; exact OracleComp.probFailure_drawList _ _ + rw [show (𝒟[dl >>= fun rest => uniformSample Chal >>= fun ch => + ids.respond pk sk st ch >>= fun oz => + (match oz with + | some z => pure ((some (w, z), []), re.cacheQuery (msg, w) ch) + | none => (fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (tapeSignBody ids M pk sk msg rest).run re : ProbComp _)]) + = 𝒟[uniformSample Chal >>= fun ch => dl >>= fun rest => + ids.respond pk sk st ch >>= fun oz => + (match oz with + | some z => pure ((some (w, z), []), re.cacheQuery (msg, w) ch) + | none => (fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (tapeSignBody ids M pk sk msg rest).run re : ProbComp _)] from + evalDist_bind_comm_probComp dl (uniformSample Chal) _] + refine evalDist_bind_congr_left (uniformSample Chal) _ _ (fun ch => ?_) + rw [show (𝒟[dl >>= fun rest => ids.respond pk sk st ch >>= fun oz => + (match oz with + | some z => pure ((some (w, z), []), re.cacheQuery (msg, w) ch) + | none => (fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (tapeSignBody ids M pk sk msg rest).run re : ProbComp _)]) + = 𝒟[ids.respond pk sk st ch >>= fun oz => dl >>= fun rest => + (match oz with + | some z => pure ((some (w, z), []), re.cacheQuery (msg, w) ch) + | none => (fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (tapeSignBody ids M pk sk msg rest).run re : ProbComp _)] from + evalDist_bind_comm_probComp dl (ids.respond pk sk st ch) _] + refine evalDist_bind_congr_left (ids.respond pk sk st ch) _ _ (fun oz => ?_) + cases oz with + | some z => rw [evalDist_bind_const_neverFails dl hdlmass] + | none => + change 𝒟[(fun rws => ((rws.1.1, w :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] = _ + rw [evalDist_map_eq_of_evalDist_eq (ih re)] + rw [map_eq_bind_pure_comp, bind_assoc] + refine evalDist_bind_congr_left dl _ _ (fun rest => ?_) + rw [map_eq_bind_pure_comp] + +omit [SampleableType Stmt] in +/-- **Expected drawn-list length of the draw-collecting signing body.** Each attempt of +`ghostSignDrawBody` records exactly one i.i.d. raw `Prod.fst <$> ids.commit pk sk` commitment; +the loop continues only on a fresh-challenge rejection (probability `≤ p` per attempt), so the +expected length of the collected list is at most `∑_{a + intro re + simp only [ghostSignDrawBody, StateT.run_pure, tsum_probOutput_pure_mul] + simp + | succ n ih => + intro re + classical + set S : ℝ≥0∞ := ∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ a with hS + have hSucc : ∑ a ∈ Finset.range (n + 1), ENNReal.ofReal p_abort ^ a = + 1 + ENNReal.ofReal p_abort * S := by + rw [Finset.sum_range_succ', pow_zero, add_comm] + congr 1 + rw [Finset.mul_sum] + exact Finset.sum_congr rfl fun a _ => pow_succ' _ _ + rw [run_ghostSignDrawBody_succ, tsum_probOutput_bind_mul] + have h_ws : ∀ ws : Commit × PrvState, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * (z.1.2.length : ℝ≥0∞)) + ≤ 1 + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * S := by + intro ws + rw [tsum_probOutput_bind_mul] + have h_ch : ∀ ch : Chal, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * (z.1.2.length : ℝ≥0∞)) + ≤ 1 + Pr[= none | ids.respond pk sk ws.2 ch] * S := by + intro ch + rw [tsum_probOutput_bind_mul] + have h_oz : ∀ oz : Option Resp, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * (z.1.2.length : ℝ≥0∞)) + ≤ 1 + (if oz = none then S else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (by simp), add_zero, tsum_probOutput_pure_mul] + simp + | none => + rw [if_pos rfl] + -- length of `ws.1 :: rws.1.2` is `1 + rws.1.2.length`; rewrite map as bind+pure. + rw [map_eq_bind_pure_comp, tsum_probOutput_bind_mul] + calc (∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (∑' y : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= y | (pure ((fun rws : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + ((rws.1.1, ws.1 :: rws.1.2), rws.2)) z) : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * (y.1.2.length : ℝ≥0∞))) + = ∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (1 + (z.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun z => ?_ + rw [tsum_probOutput_pure_mul] + simp only [List.length_cons] + push_cast + ring_nf + _ = (∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re]) + + ∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (z.1.2.length : ℝ≥0∞) := by + rw [← ENNReal.tsum_add] + exact tsum_congr fun z => by rw [mul_add, mul_one] + _ ≤ 1 + S := add_le_add tsum_probOutput_le_one (ih re) + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_oz) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by simp [hoz]] + simp [mul_comm] + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ch) ?_ + refine add_le_add_right (le_of_eq ?_) _ + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact tsum_congr fun ch => (mul_assoc _ _ _).symm + refine le_trans (tsum_probOutput_mul_le_add_of_le _ h_ws) ?_ + rw [hSucc] + gcongr + calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * S) + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * S := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * S := + mul_le_mul_left (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + +omit [SampleableType Stmt] in +/-- **Tight expected drawn-list length of the draw-collecting signing body.** Sharper companion to +`tsum_probOutput_run_ghostSignDrawBody_mul_length_le`: the expected number of *recorded* (rejected) +commitments of one `ghostSignDrawBody` run is at most the *reject-gated* geometric sum +`∑_{a + intro re + simp only [ghostSignDrawBody, StateT.run_pure, tsum_probOutput_pure_mul] + simp + | succ n ih => + intro re + classical + set S : ℝ≥0∞ := ∑ a ∈ Finset.range n, ENNReal.ofReal p_abort ^ (a + 1) with hS + -- Target: the `(n+1)`-attempt reject-count expectation is `≤ ofReal p * (1 + S)`, which + -- equals `∑_{a by rw [← pow_succ'] + rw [hSucc, run_ghostSignDrawBody_succ, tsum_probOutput_bind_mul] + -- Per-commit-draw `ws`: the recorded list is empty on accept and `ws.1 :: recursive` on + -- reject; reject happens with probability `Pr[= none | uniformSample >>= respond]`. + have h_ws : ∀ ws : Commit × PrvState, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * (z.1.2.length : ℝ≥0∞)) + ≤ Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * (1 + S) := by + intro ws + rw [tsum_probOutput_bind_mul] + have h_ch : ∀ ch : Chal, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * (z.1.2.length : ℝ≥0∞)) + ≤ Pr[= none | ids.respond pk sk ws.2 ch] * (1 + S) := by + intro ch + rw [tsum_probOutput_bind_mul] + -- Per response `oz`: accept contributes `0`, reject contributes `1 + S`. + have h_oz : ∀ oz : Option Resp, + (∑' z : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * (z.1.2.length : ℝ≥0∞)) + ≤ (if oz = none then (1 : ℝ≥0∞) + S else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (by simp), tsum_probOutput_pure_mul] + simp [List.length] + | none => + rw [if_pos rfl, map_eq_bind_pure_comp, tsum_probOutput_bind_mul] + calc (∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (∑' y : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= y | (pure ((fun rws : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + ((rws.1.1, ws.1 :: rws.1.2), rws.2)) z) : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * (y.1.2.length : ℝ≥0∞))) + = ∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (1 + (z.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun z => ?_ + rw [tsum_probOutput_pure_mul] + simp only [List.length_cons] + push_cast + ring_nf + _ = (∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re]) + + ∑' z : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= z | (ghostSignDrawBody ids M pk sk msg n).run re] * + (z.1.2.length : ℝ≥0∞) := by + rw [← ENNReal.tsum_add] + exact tsum_congr fun z => by rw [mul_add, mul_one] + _ ≤ 1 + S := add_le_add tsum_probOutput_le_one (ih re) + refine le_trans (ENNReal.tsum_le_tsum fun oz => + mul_le_mul_right (h_oz oz) _) ?_ + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by + rw [if_neg hoz, mul_zero]] + rw [if_pos rfl, mul_comm] + refine le_trans (ENNReal.tsum_le_tsum fun ch => + mul_le_mul_right (h_ch ch) _) ?_ + rw [probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + exact le_of_eq (tsum_congr fun ch => (mul_assoc _ _ _).symm) + refine le_trans (ENNReal.tsum_le_tsum fun ws => mul_le_mul_right (h_ws ws) _) ?_ + calc ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + (Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch] * (1 + S)) + = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + Pr[= none | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch]) * (1 + S) := by + rw [← ENNReal.tsum_mul_right] + exact tsum_congr fun ws => (mul_assoc _ _ _).symm + _ ≤ ENNReal.ofReal p_abort * (1 + S) := + mul_le_mul_left (tsum_probOutput_commit_mul_abort_le ids pk sk hAbort) _ + +omit [SampleableType Stmt] in +/-- **Single-signing-body resampling over-count.** Testing the *drawn list* produced by one +`ghostSignDrawBody` run (the rejected-attempt commitments, write-only side-data) against any *fixed* +read strategy `σ` with `q` reads fires with probability at most testing `n` *fresh* i.i.d. raw +`Prod.fst <$> ids.commit pk sk` draws against the same strategy (`drawList … n`, where `n` is the +attempt budget `maxAttempts`). + +This is the genuine per-query resampling content, isolated to one signing body. The drawn list of +the body is *not* equal in law to `n` fresh raw draws (the rejected draws are skewed by the +rejection-conditioning `commit | reject`), but the firing event over-counts to the fresh game: on +the accept branch the body records *no* commitment (so its read-game fires with probability `0`, +dominated by the fresh side, which still draws and tests one value); on the reject branch the body's +recorded commitment is a raw `Prod.fst <$> ids.commit pk sk` draw — distributed exactly as the fresh +head — and its `readMany` test matches the fresh head's, while the recursive rejected list is +dominated by the recursive fresh list (induction). The read strategy `σ` is *fixed* (the read points +are determined by the all-miss reply history; the drawn values never feed them — value-freeness), +which is what lets a single `σ` dominate both sides. The corresponding fold-level statement — +front-loading every signing query's interleaved draws into one aggregate `drawList` block — is +`evalDist_deferredDrawRead_eq_drawList_tapeDrawRead`, which the headline uses on the σ-free +first-moment route. This lemma is the single-body over-count in σ-indexed form; it is not on the +live headline path (which charges the expected coincidence count directly). -/ +lemma ghostSignDrawBody_readManyList_le_drawList (pk : Stmt) (sk : Wit) (msg : M) + (q : ℕ) (σ : List Bool → Commit) : + ∀ (n : ℕ) (re : (M × Commit →ₒ Chal).QueryCache), + Pr[(fun b : Bool => b = true) | + (ghostSignDrawBody ids M pk sk msg n).run re >>= fun rws => + pure (OracleComp.readManyList rws.1.2 q σ)] + ≤ Pr[(fun b : Bool => b = true) | + OracleComp.drawList (Prod.fst <$> ids.commit pk sk) n >>= fun ws => + pure (OracleComp.readManyList ws q σ)] := by + intro n + induction n with + | zero => + intro re + simp [ghostSignDrawBody, OracleComp.drawList, OracleComp.readManyList] + | succ n ih => + intro re + -- Unfold one attempt on the left and one fresh draw on the right; both bind over the same + -- raw `ids.commit pk sk` draw, so compare the per-draw fire-marginals termwise. + rw [run_ghostSignDrawBody_succ] + rw [OracleComp.drawList, bind_assoc, bind_map_left] + simp only [bind_assoc, pure_bind] + rw [probEvent_bind_eq_tsum, probEvent_bind_eq_tsum] + refine ENNReal.tsum_le_tsum fun ws => ?_ + gcongr + -- Per commit draw `ws`: name the recursive fresh `n`-draw game and the recursive body-`n` + -- game; the latter is `≤` the former by the inductive hypothesis (`ih`). + set RHSinner : ℝ≥0∞ := Pr[(fun b : Bool => b = true) | + OracleComp.drawList (Prod.fst <$> ids.commit pk sk) n >>= fun rest => + pure (OracleComp.readManyList rest q σ)] with hRHSinner + by_cases hhead : OracleComp.readMany ws.1 q σ = true + · -- The head already fires: the RHS `readManyList (ws.1 :: rest)` is always `true`, so the + -- RHS per-draw marginal is the full mass of `drawList n` = 1 ≥ the LHS. + refine le_trans probEvent_le_one (le_of_eq ?_) + symm + have hcongr : (OracleComp.drawList (Prod.fst <$> ids.commit pk sk) n >>= fun rest => + pure (OracleComp.readManyList (ws.1 :: rest) q σ)) + = (OracleComp.drawList (Prod.fst <$> ids.commit pk sk) n >>= fun _ => + (pure true : ProbComp Bool)) := by + refine bind_congr fun rest => ?_ + rw [OracleComp.readManyList, List.any_cons, hhead, Bool.true_or] + rw [hcongr, probEvent_bind_eq_tsum] + simp only [probEvent_pure, if_pos] + rw [ENNReal.tsum_mul_right, OracleComp.tsum_probOutput_drawList_eq_one, one_mul] + · -- The head misses: the RHS reduces to the recursive fresh game `RHSinner`, and the LHS is + -- dominated by the recursive body-`n` game, which is `≤ RHSinner` by `ih`. + rw [Bool.not_eq_true] at hhead + have hRHS : Pr[(fun b : Bool => b = true) | + OracleComp.drawList (Prod.fst <$> ids.commit pk sk) n >>= fun rest => + pure (OracleComp.readManyList (ws.1 :: rest) q σ)] = RHSinner := by + rw [hRHSinner] + refine probEvent_bind_congr fun rest _ => ?_ + rw [OracleComp.readManyList, List.any_cons, hhead, Bool.false_or, OracleComp.readManyList] + rw [hRHS] + -- The LHS per-draw game is dominated by the recursive body-`n` game: drop the + -- `uniformSample`/`respond` draws (mass `≤ 1`); the accept branch records `[]` + -- (`readManyList [] = false`, fires with probability `0`) and the reject branch's head + -- test `readMany ws.1 q σ` misses (`hhead`), so its `readManyList (ws.1 :: inner)` reduces + -- to the body-`n` game's `readManyList inner`. + refine le_trans ?_ (ih re) + refine probEvent_bind_le_of_forall_le fun ch _ => ?_ + refine probEvent_bind_le_of_forall_le fun oz _ => ?_ + cases oz with + | some z => simp [OracleComp.readManyList] + | none => + rw [bind_map_left] + refine le_of_eq ?_ + refine probEvent_bind_congr fun rws _ => ?_ + rw [OracleComp.readManyList, List.any_cons, hhead, Bool.false_or, + OracleComp.readManyList] + +/-- The deferred-draw handler for the adversary's oracles, driving the distribution-level mono +skeleton against `ghostBlindImpl`. Carries the accumulated drawn-commitment list and a monotone +read-hit flag in place of the eager ghost cache (see `DeferredState`). -/ +noncomputable def deferredDrawImpl (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (DeferredState M Commit Chal) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + (fun cu => (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || decide (mc.2 ∈ s.1.2)))) <$> + roStep M s.1.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc => (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1 + +omit [SampleableType Stmt] in +/-- **Per-step expected drawn-list length growth of the deferred-draw handler.** One step of +`deferredDrawImpl` grows the expected drawn-list length by at most `1/(1-p)` on a signing query and +by `0` on a uniform or random-oracle-read query (which leave the drawn list untouched). The +signing-step bound is the per-query draw count `tsum_probOutput_run_ghostSignDrawBody_mul_length_le` +folded with `geomAttemptSum_le`. This is the per-step charge that the run-level mean fold +`deferredDraw_run_expected_length_le` telescopes against `signHashQueryBound`. -/ +lemma deferredDrawImpl_step_expected_length_le (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : DeferredState M Commit Chal) : + (∑' z : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) × + DeferredState M Commit Chal, + Pr[= z | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * (z.2.1.2.length : ℝ≥0∞)) + ≤ (s.1.2.length : ℝ≥0∞) + + (if (t matches Sum.inr _) then ENNReal.ofReal (1 / (1 - p_abort)) else 0) := by + classical + rcases t with (n | mc) | msg + · -- UNIFORM: state untouched, drawn list `s.1.2` preserved. + rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ (by simp [deferredDrawImpl])) + intro z hz + have hzs : z ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hz + rw [support_map] at hzs + obtain ⟨u, _, rfl⟩ := hzs; rfl + · -- READ: writes only the base cache / bad flag; drawn list `s.1.2` preserved. + rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ ?_) + · intro z hz + have hzs : z ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || decide (mc.2 ∈ s.1.2)))) <$> + roStep M s.1.1.1 mc) := hz + rw [support_map] at hzs + obtain ⟨cu, _, rfl⟩ := hzs; rfl + · simp only [deferredDrawImpl, StateT.run_mk] + rcases hg : s.1.1.1 mc with _ | v <;> simp [roStep, hg] + · -- SIGN: drawn list becomes `s.1.2 ++ alc.1.2`; expected new length ≤ 1/(1-p). + rw [if_pos (by simp)] + have hrun : (deferredDrawImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1 := rfl + rw [hrun] + refine le_of_eq_of_le (tsum_probOutput_map_mul + ((ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1) + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) + (fun z => (z.2.1.2.length : ℝ≥0∞))) ?_ + calc _ + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + ((s.1.2.length : ℝ≥0∞) + (alc.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun alc => ?_ + simp only [List.length_append] + push_cast + ring + _ = (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + (s.1.2.length : ℝ≥0∞)) + + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + (alc.1.2.length : ℝ≥0∞) := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun alc => by rw [mul_add] + _ ≤ (s.1.2.length : ℝ≥0∞) + ENNReal.ofReal (1 / (1 - p_abort)) := by + refine add_le_add ?_ ?_ + · rw [ENNReal.tsum_mul_right, tsum_probOutput_eq_one' (by simp), one_mul] + · exact le_trans (tsum_probOutput_run_ghostSignDrawBody_mul_length_le ids M pk sk msg + hAbort maxAttempts s.1.1.1) (geomAttemptSum_le maxAttempts hp₀ hp) + +omit [SampleableType Stmt] in +/-- **Sign-step coupling.** The eager ghost signing body `ghostSignBody` and the deferred draw- +collecting body `ghostSignDrawBody` are coupled, with their `ids.commit`/`uniformSample`/`respond` +draws matched, so that the outputs and real caches agree and the eager ghost layer's key domain is +covered by the front drawn list `drawn` extended with the body's collected commitments. Proved by +induction on the attempt budget: the accept branch writes the accepted commitment to both real +caches and leaves the ghost layer covered by `drawn`; the reject branch records the commitment in +the ghost layer and prepends it to the deferred collected list, recursing with a wider cover. -/ +theorem signBody_couple (pk : Stmt) (sk : Wit) (msg : M) : + ∀ (n : ℕ) (re gh : (M × Commit →ₒ Chal).QueryCache) (drawn : List Commit), + (∀ mc : M × Commit, gh mc ≠ none → mc.2 ∈ drawn) → + OracleComp.ProgramLogic.Relational.RelTriple + ((ghostSignBody ids M pk sk msg n).run (re, gh)) + ((ghostSignDrawBody ids M pk sk msg n).run re) + (fun p₁ p₂ => p₁.1 = p₂.1.1 ∧ p₁.2.1 = p₂.2 ∧ + (∀ mc : M × Commit, p₁.2.2 mc ≠ none → mc.2 ∈ drawn ++ p₂.1.2)) + | 0, re, gh, drawn, hcov => by + simp only [ghostSignBody, ghostSignDrawBody, StateT.run_pure] + exact OracleComp.ProgramLogic.Relational.relTriple_pure_pure + ⟨rfl, rfl, fun mc hmc => List.mem_append.2 (Or.inl (hcov mc hmc))⟩ + | (n+1), re, gh, drawn, hcov => by + have hrun₁ : (ghostSignBody ids M pk sk msg (n+1)).run (re, gh) = + (ids.commit pk sk >>= fun wst => uniformSample Chal >>= fun c => + ids.respond pk sk wst.2 c >>= fun oz => + match oz with + | some z => pure (some (wst.1, z), + (re.cacheQuery (msg, wst.1) c, uncacheQuery M gh (msg, wst.1))) + | none => (ghostSignBody ids M pk sk msg n).run + (re, gh.cacheQuery (msg, wst.1) c)) := by + simp only [ghostSignBody, bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, + pure_bind] + refine congrArg (ids.commit pk sk >>= ·) (funext fun wst => ?_) + refine congrArg (uniformSample Chal >>= ·) (funext fun c => ?_) + refine congrArg (ids.respond pk sk wst.2 c >>= ·) (funext fun oz => ?_) + cases oz with + | some z => simp [StateT.run_modify] + | none => simp [StateT.run_bind, StateT.run_modify] + have hrun₂ : (ghostSignDrawBody ids M pk sk msg (n+1)).run re = + (ids.commit pk sk >>= fun wst => uniformSample Chal >>= fun c => + ids.respond pk sk wst.2 c >>= fun oz => + match oz with + | some z => pure ((some (wst.1, z), []), re.cacheQuery (msg, wst.1) c) + | none => (fun rws => ((rws.1.1, wst.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re) := by + simp only [ghostSignDrawBody, bind_assoc, StateT.run_bind, OracleComp.liftM_run_StateT, + pure_bind] + refine congrArg (ids.commit pk sk >>= ·) (funext fun wst => ?_) + refine congrArg (uniformSample Chal >>= ·) (funext fun c => ?_) + refine congrArg (ids.respond pk sk wst.2 c >>= ·) (funext fun oz => ?_) + cases oz with + | some z => simp [StateT.run_modify] + | none => simp [StateT.run_bind, StateT.run_pure, map_eq_bind_pure_comp, Function.comp] + rw [hrun₁, hrun₂] + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_ + rintro wst _ (rfl : wst = _) + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_ + rintro c _ (rfl : c = _) + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_ + rintro oz _ (rfl : oz = _) + cases oz with + | some z => + refine OracleComp.ProgramLogic.Relational.relTriple_pure_pure ⟨rfl, rfl, ?_⟩ + intro mc hmc + -- accept branch: ghost layer is `uncacheQuery M gh (msg, wst.1)`, whose domain ⊆ dom gh + refine List.mem_append.2 (Or.inl (hcov mc ?_)) + by_cases hmceq : mc = (msg, wst.1) + · exact absurd (by simp [uncacheQuery, hmceq]) hmc + · simpa [uncacheQuery, hmceq] using hmc + | none => + have hcov' : ∀ mc : M × Commit, (gh.cacheQuery (msg, wst.1) c) mc ≠ none → + mc.2 ∈ drawn ++ [wst.1] := by + intro mc hmc + by_cases hmceq : mc = (msg, wst.1) + · subst hmceq; exact List.mem_append.2 (Or.inr (by simp)) + · rw [QueryCache.cacheQuery_of_ne _ _ hmceq] at hmc + exact List.mem_append.2 (Or.inl (hcov mc hmc)) + have hih := signBody_couple pk sk msg n re (gh.cacheQuery (msg, wst.1) c) + (drawn ++ [wst.1]) hcov' + rw [show ((fun rws : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => ((rws.1.1, wst.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re) + = ((ghostSignDrawBody ids M pk sk msg n).run re >>= fun rws => + pure ((rws.1.1, wst.1 :: rws.1.2), rws.2)) from by rw [map_eq_bind_pure_comp]; rfl] + rw [show ((ghostSignBody ids M pk sk msg n).run (re, gh.cacheQuery (msg, wst.1) c)) + = ((ghostSignBody ids M pk sk msg n).run (re, gh.cacheQuery (msg, wst.1) c) >>= pure) + from by rw [bind_pure]] + refine OracleComp.ProgramLogic.Relational.relTriple_bind hih ?_ + rintro p₁ p₂ ⟨hout, hcache, hghcov⟩ + refine OracleComp.ProgramLogic.Relational.relTriple_pure_pure ⟨hout, hcache, ?_⟩ + intro mc hmc + have hmem := hghcov mc hmc + rw [List.append_assoc] at hmem + simpa using hmem + +/-! ### Stage 3a: the deferred-coupling reduction (Piece A) + +The eager ghost-blind bad marginal reduces, through the deferred-draw handler `deferredDrawImpl`, +to the deferred run's bad marginal: + +* **Piece A** (`ghostBlind_bad_le_deferredDraw`): a *pointwise coupling* of the eager ghost-blind + run with the deferred-draw run, established on the pointwise mono skeleton + `relTriple_simulateQ_run_mono` carrying the state invariant `deferredCoupleInv` (real cache and + signed list equal; ghost domain covered by the drawn-commitment list; bad-flag ordered). The + read step is an output-equal coupling (both answer from the real layer via `roStep`, and the + membership flag fires more readily on the deferred side because it ignores the message component); + the sign step couples the two bodies' `ids.commit` draws so the eager ghost writes and the + deferred draws stay in lockstep. `probEvent_le_of_relTriple_imp` then reads off the ordered bad + marginals. + +The deferred run's bad marginal is then carried — through the read-recording reduction +(`deferredDraw_bad_le_readRecord`) and the first-moment Markov step +(`readRecord_pred_le_expected_coincidences`) — to the expected coincidence count bounded by +`readRecord_expected_coincidences_le`. + +This reduction uses the pointwise coupling because the bad flags *are* pointwise linkable (eager +ghost-membership ⟹ deferred commitment-membership, since the drawn list grows in lockstep with the +ghost cache), so the pointwise `relTriple_simulateQ_run_mono` route applies. The value-free charge +that this reduction feeds into is `readRecord_expected_coincidences_le`. + +The state invariant linking the eager `GhostState` and the deferred `DeferredState`: real cache and +signed-message list agree, every key in the ghost cache has its commitment recorded in the drawn +list, and the bad flag is ordered (eager-bad ⟹ deferred-bad). The read points coincide because both +sides answer from the (shared) real layer. -/ +omit [SampleableType Stmt] in +/-- The coupling invariant between the eager ghost-blind state and the deferred-draw state: real +cache and signed list agree, every ghost-cache key's commitment is in the drawn list, and the bad +flag is ordered. -/ +def deferredCoupleInv + (s₁ : GhostState M Commit Chal) (s₂ : DeferredState M Commit Chal) : Prop := + s₁.1.1.1 = s₂.1.1.1 ∧ s₁.1.2 = s₂.1.1.2 ∧ + (∀ mc : M × Commit, s₁.1.1.2 mc ≠ none → mc.2 ∈ s₂.1.2) ∧ + (s₁.2 = true → s₂.2 = true) + +omit [SampleableType Stmt] in +/-- **Per-query coupling step for the ghost-blind → deferred coupling.** From any pair of +`deferredCoupleInv`-related states, one step of the eager ghost-blind handler couples with one step +of the deferred-draw handler with equal output and the invariant preserved. + +* **Uniform** steps forward the same draw; the state is untouched, so the invariant is inherited. +* **Read** steps answer from the shared real layer via `roStep` (same answer, same cache update); + the eager bad flag fires on ghost-domain membership and the deferred one on drawn-list membership; + the domain-coverage invariant makes the eager fire imply the deferred fire (it ignores the message + component), preserving the bad ordering. +* **Sign** steps invoke `signBody_couple`: the matched `ids.commit` draws keep the outputs and real + caches equal and extend the drawn list to cover the new ghost writes; the bad flag is intact. -/ +theorem deferredCouple_step (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (u₁ : GhostState M Commit Chal) (u₂ : DeferredState M Commit Chal) + (hu : deferredCoupleInv M u₁ u₂) : + OracleComp.ProgramLogic.Relational.RelTriple + ((ghostBlindImpl ids M maxAttempts pk sk t).run u₁) + ((deferredDrawImpl ids M maxAttempts pk sk t).run u₂) + (fun p₁ p₂ => p₁.1 = p₂.1 ∧ deferredCoupleInv M p₁.2 p₂.2) := by + obtain ⟨hre, hl, hdom, hbad⟩ := hu + rcases t with (n | mc) | msg + · -- UNIFORM: both forward the same draw; state untouched. + have hrun₁ : (ghostBlindImpl ids M maxAttempts pk sk (.inl (.inl n))).run u₁ = + (fun u => (u, u₁)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + have hrun₂ : (deferredDrawImpl ids M maxAttempts pk sk (.inl (.inl n))).run u₂ = + (fun u => (u, u₂)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + rw [hrun₁, hrun₂] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + exact ⟨rfl, hre, hl, hdom, hbad⟩ + · -- READ: answer from the shared real layer; bad flag dominated under domain coverage. + have hrun₂ : (deferredDrawImpl ids M maxAttempts pk sk (.inl (.inr mc))).run u₂ = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, u₂.1.1.2), u₂.1.2), u₂.2 || decide (mc.2 ∈ u₂.1.2)))) <$> + roStep M u₂.1.1.1 mc := rfl + cases hgh : u₁.1.1.2 mc with + | none => + have hrun₁ : (ghostBlindImpl ids M maxAttempts pk sk (.inl (.inr mc))).run u₁ = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, u₁.1.1.2), u₁.1.2), u₁.2))) <$> roStep M u₁.1.1.1 mc := by + rw [ghostBlindImpl_eq_ghostHybridImpl_false] + exact ghostHybridImpl_run_ro_ghost_none ids M maxAttempts false pk sk hgh + rw [hrun₁, hrun₂, hre] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + exact ⟨rfl, rfl, hl, hdom, fun hb => by rw [hbad hb]; rfl⟩ + | some v => + have hgh2 : u₁.1.1.2 mc ≠ none := by rw [hgh]; exact Option.some_ne_none v + have hrun₁ : (ghostBlindImpl ids M maxAttempts pk sk (.inl (.inr mc))).run u₁ = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, u₁.1.1.2), u₁.1.2), true))) <$> roStep M u₁.1.1.1 mc := by + rw [ghostBlindImpl_eq_ghostHybridImpl_false, + ghostHybridImpl_run_ro_ghost_some ids M maxAttempts false pk sk hgh, + if_neg Bool.false_ne_true] + rw [hrun₁, hrun₂, hre] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + have hdef : (u₂.2 || decide (mc.2 ∈ u₂.1.2)) = true := by simp [hdom mc hgh2] + exact ⟨rfl, rfl, hl, hdom, fun _ => hdef⟩ + · -- SIGN: couple the two signing bodies via `signBody_couple`; bad flag untouched. + have hrun₁ : (ghostBlindImpl ids M maxAttempts pk sk (.inr msg)).run u₁ = + (fun alc : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (alc.1, ((alc.2, msg :: u₁.1.2), u₁.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run u₁.1.1 := by + rw [ghostBlindImpl_eq_ghostHybridImpl_false] + exact ghostHybridImpl_run_sign ids M maxAttempts false pk sk msg u₁ + have hrun₂ : (deferredDrawImpl ids M maxAttempts pk sk (.inr msg)).run u₂ = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: u₂.1.1.2), u₂.1.2 ++ alc.1.2), u₂.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run u₂.1.1.1 := rfl + rw [hrun₁, hrun₂] + have hu11 : u₁.1.1 = (u₂.1.1.1, u₁.1.1.2) := by rw [← hre] + rw [hu11] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (signBody_couple ids M pk sk msg maxAttempts u₂.1.1.1 u₁.1.1.2 u₂.1.2 hdom) ?_) + rintro p₁ p₂ ⟨hout, hcache, hghcov⟩ + exact ⟨hout, hcache, by rw [hl], hghcov, hbad⟩ + +omit [SampleableType Stmt] in +/-- **The ghost-blind → deferred run coupling.** By induction on the adversary computation `oa`, +the eager ghost-blind run and the deferred-draw run are coupled with the invariant +`deferredCoupleInv` preserved at every leaf, using `deferredCouple_step` at each query and the +inductive hypothesis for the continuation. -/ +theorem deferredCouple_run {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s₁ : GhostState M Commit Chal) (s₂ : DeferredState M Commit Chal), + deferredCoupleInv M s₁ s₂ → + OracleComp.ProgramLogic.Relational.RelTriple + ((simulateQ (ghostBlindImpl ids M maxAttempts pk sk) oa).run s₁) + ((simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s₂) + (fun q₁ q₂ => deferredCoupleInv M q₁.2 q₂.2) := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s₁ s₂ hinv + simp only [simulateQ_pure, StateT.run_pure] + exact OracleComp.ProgramLogic.Relational.relTriple_pure_pure hinv + | query_bind t ob ih => + intro s₁ s₂ hinv + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (deferredCouple_step ids M maxAttempts pk sk t s₁ s₂ hinv) ?_ + rintro p₁ p₂ ⟨hout, hinv'⟩ + rw [show p₁.1 = p₂.1 from hout] + exact ih p₂.1 p₁.2 p₂.2 hinv' + +omit [SampleableType Stmt] in +/-- **Piece A: the ghost-blind → deferred coupling.** The ghost-blind run's bad marginal is at most +the deferred-draw run's bad marginal, from any pair of `deferredCoupleInv`-related start states. + +Reads off the bad-flag ordering component of the invariant from the run coupling +`deferredCouple_run` via `probEvent_le_of_relTriple_imp`. -/ +theorem ghostBlind_bad_le_deferredDraw {γ : Type} + (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (s₁ : GhostState M Commit Chal) (s₂ : DeferredState M Commit Chal) + (hinv : deferredCoupleInv M s₁ s₂) : + Pr[fun z : γ × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) oa).run s₁] + ≤ Pr[fun z : γ × DeferredState M Commit Chal => z.2.2 = true | + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s₂] := + OracleComp.ProgramLogic.Relational.probEvent_le_of_relTriple_imp + (deferredCouple_run ids M maxAttempts pk sk oa s₁ s₂ hinv) + (fun _ _ hp => hp.2.2.2) + +omit [SampleableType Stmt] in +/-- **The drawn list only grows.** Every reachable final state of the deferred-draw run from a +start state `s` has the start's drawn list `s.1.2` as a prefix: uniform and read steps leave the +drawn list untouched, and a signing step appends (`s.1.2 ++ alc.1.2`). Hence the number of *new* +draws is `final.length - s.1.2.length` and is well-behaved (`s.1.2.length ≤ final.length`). -/ +theorem deferredDraw_run_drawn_prefix {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredState M Commit Chal) + (z : γ × DeferredState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s) → + s.1.2 <+: z.2.1.2 := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz; exact List.prefix_rfl + | query_bind t ob ih => + intro s z hz + rw [simulateQ_query_bind, StateT.run_bind, mem_support_bind_iff] at hz + obtain ⟨x, hx, hzx⟩ := hz + refine List.IsPrefix.trans ?_ (ih x.1 x.2 z hzx) + -- The step's output drawn list extends `s.1.2`. + rcases t with (n | mc) | msg + · have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs; exact List.prefix_rfl + · have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || decide (mc.2 ∈ s.1.2)))) <$> + roStep M s.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs; exact List.prefix_rfl + · have hxs : x ∈ support ((fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1) := hx + rw [support_map] at hxs + obtain ⟨alc, _, rfl⟩ := hxs; exact List.prefix_append s.1.2 alc.1.2 + +omit [SampleableType Stmt] in +/-- **The signed-message list only grows in length.** Every reachable final state of the +deferred-draw run from a start state `s` has signed-message list at least as long as the start's +`s.1.1.2`: uniform and read steps leave it untouched, and a signing step prepends one message +(`msg :: s.1.1.2`). Hence the number of *new* signing queries is `final.length - s.1.1.2.length`. -/ +theorem deferredDraw_run_signed_prefix {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredState M Commit Chal) + (z : γ × DeferredState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s) → + s.1.1.2.length ≤ z.2.1.1.2.length := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz; exact le_rfl + | query_bind t ob ih => + intro s z hz + rw [simulateQ_query_bind, StateT.run_bind, mem_support_bind_iff] at hz + obtain ⟨x, hx, hzx⟩ := hz + refine le_trans ?_ (ih x.1 x.2 z hzx) + rcases t with (n | mc) | msg + · have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || decide (mc.2 ∈ s.1.2)))) <$> + roStep M s.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1) := hx + rw [support_map] at hxs + obtain ⟨alc, _, rfl⟩ := hxs; simp + +omit [SampleableType Stmt] in +/-- **Run-level expected drawn-list length of the deferred-draw run.** By induction on the +adversary computation `oa`, the expected final drawn-list length of the deferred-draw run from a +start state `s` is at most `s.1.2.length + qSrem · (1/(1-p))`, where `qSrem` bounds the number of +signing queries `oa` makes (the `(· matches .inr _)` component of `signHashQueryBound`). Each +signing query grows the expected drawn length by at most `1/(1-p)` (the per-step charge +`deferredDrawImpl_step_expected_length_le`), and uniform/read queries leave it unchanged; the +signing-query budget `qSrem` telescopes across the fold exactly as in +`IsQueryBoundP.simulateQ_run_StateT_of_step`. This is the mean bound that the constructed count law +`kn` of Piece B inherits. -/ +theorem deferredDraw_run_expected_length_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qSrem : ℕ), oa.IsQueryBoundP (· matches Sum.inr _) qSrem → + ∀ (s : DeferredState M Commit Chal), + (∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s] * + (z.2.1.2.length : ℝ≥0∞)) + ≤ (s.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + induction oa using OracleComp.inductionOn with + | pure a => + intro qSrem _ s + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + exact le_self_add + | query_bind t ob ih => + intro qSrem hQ s + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rw [simulateQ_query_bind, StateT.run_bind, tsum_probOutput_bind_mul] + set c : ℝ≥0∞ := ENNReal.ofReal (1 / (1 - p_abort)) with hc + -- Total mass of one deferred step is `1` (no failure). + have hmass : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredState M Commit Chal, + Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s]) = 1 := + tsum_probOutput_eq_one' (by + rcases t with (n | mc) | msg + · simp [deferredDrawImpl] + · simp only [deferredDrawImpl, StateT.run_mk] + rcases hg : s.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawImpl]) + -- Abstract the continuation's carried budget `b` and the per-step abort charge. + -- Generic combiner: with continuation bound `≤ x.length + b·c`, step charge `extra`, + -- and `extra + b·c ≤ qSrem·c`, the fold gives the run bound. + have hfold : ∀ (b : ℕ) (extra : ℝ≥0∞), + (∀ x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredState M Commit Chal, + (∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (z.2.1.2.length : ℝ≥0∞)) + ≤ (x.2.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c) → + (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.2.length : ℝ≥0∞)) ≤ (s.1.2.length : ℝ≥0∞) + extra → + extra + (b : ℝ≥0∞) * c ≤ (qSrem : ℝ≥0∞) * c → + (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (z.2.1.2.length : ℝ≥0∞)) + ≤ (s.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + intro b extra hcont hstep hbudget + calc (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * (z.2.1.2.length : ℝ≥0∞)) + ≤ ∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c) := + ENNReal.tsum_le_tsum fun x => by gcongr; exact hcont x + _ = (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.2.length : ℝ≥0∞)) + (b : ℝ≥0∞) * c := by + rw [show (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c)) + = ∑' x, (Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.2.length : ℝ≥0∞) + + Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((b : ℝ≥0∞) * c)) from tsum_congr fun x => by rw [mul_add]] + rw [ENNReal.tsum_add, ENNReal.tsum_mul_right, hmass, one_mul] + _ ≤ ((s.1.2.length : ℝ≥0∞) + extra) + (b : ℝ≥0∞) * c := by gcongr + _ ≤ (s.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by rw [add_assoc]; gcongr + -- Case on the query head so the `if p t` budget/charge reduce concretely. + rcases t with (n | mc) | msg + · -- UNIFORM: budget unchanged, no charge. + refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawImpl_step_expected_length_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inl n)) s + · -- READ: budget unchanged, no charge. + refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawImpl_step_expected_length_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inr mc)) s + · -- SIGN: budget decrements; `0 < qSrem`, charge `c`, recombine `c + (qSrem-1)·c = qSrem·c`. + have hpos : 0 < qSrem := by + rcases hQ1 with hno | hpos + · exact absurd (by simp) hno + · exact hpos + refine hfold (qSrem - 1) c (fun x => ih x.1 (qSrem - 1) (by simpa using hQ2 x.1) x.2) ?_ ?_ + · have hstep := deferredDrawImpl_step_expected_length_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inr msg) s + rwa [if_pos (by rfl), ← hc] at hstep + · rw [add_comm, ← add_one_mul, + show ((qSrem - 1 : ℕ) : ℝ≥0∞) + 1 = (qSrem : ℝ≥0∞) by + have : qSrem - 1 + 1 = qSrem := by omega + rw [← this]; push_cast; ring] + +omit [SampleableType Stmt] in +/-- **The deferred-draw run never fails.** Every step of `deferredDrawImpl` is a pushforward of a +non-failing `ProbComp` (uniform sampling, `roStep`, or the draw-collecting signing body), so the +whole `simulateQ` fold has zero failure mass. -/ +theorem deferredDraw_run_neverFail {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredState M Commit Chal), + Pr[⊥ | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s] = 0 := by + induction oa using OracleComp.inductionOn with + | pure a => intro s; simp [simulateQ_pure] + | query_bind t ob ih => + intro s + rw [simulateQ_query_bind, StateT.run_bind, probFailure_bind_eq_zero_iff] + refine ⟨?_, fun x _ => ih x.1 x.2⟩ + rcases t with (n | mc) | msg + · simp [deferredDrawImpl] + · simp only [deferredDrawImpl] + rcases hg : s.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawImpl] + +omit [SampleableType Stmt] in +/-- **The constructed count law of Piece B and its mean bound.** Mapping the deferred-draw run to +its number of *new* draws beyond the start prefix `ws₀` (i.e. `final.length - ws₀.length`) gives a +count law `kn` whose mean is at most `qSrem/(1-p)`. The mean equals the expected total drawn length +minus `ws₀.length` (valid because `ws₀` is always a prefix, `deferredDraw_run_drawn_prefix`), and +the expected total length is bounded by `ws₀.length + qSrem·(1/(1-p))` +(`deferredDraw_run_expected_length_le`), so the `ws₀.length` cancels. This is the mean obligation of +Piece B, discharged for the constructed `kn`. -/ +theorem deferredDraw_kn_mean_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (qSrem : ℕ) (hQ : oa.IsQueryBoundP (· matches Sum.inr _) qSrem) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) (ws₀ : List Commit) : + (∑' n : ℕ, Pr[= n | + (fun z : γ × DeferredState M Commit Chal => z.2.1.2.length - ws₀.length) <$> + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run (((re, l), ws₀), false)] * + (n : ℝ≥0∞)) + ≤ ENNReal.ofReal ((qSrem : ℝ) / (1 - p_abort)) := by + classical + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + set run : ProbComp (γ × DeferredState M Commit Chal) := + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run (((re, l), ws₀), false) with hrun + -- The mean of `kn` equals the run-expectation of the new-draw count. + have hmean : (∑' n : ℕ, Pr[= n | + (fun z : γ × DeferredState M Commit Chal => z.2.1.2.length - ws₀.length) <$> run] * + (n : ℝ≥0∞)) + = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞) := + tsum_probOutput_map_mul run + (fun z => z.2.1.2.length - ws₀.length) (fun n => (n : ℝ≥0∞)) + rw [hmean] + -- Add back `ws₀.length` to recover the total-length expectation, bounded by the fold lemma. + have hsplit : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞)) + (ws₀.length : ℝ≥0∞) + ≤ (ws₀.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + have hmass : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run]) = 1 := by + rw [hrun] + exact tsum_probOutput_eq_one' + (deferredDraw_run_neverFail ids M maxAttempts pk sk oa (((re, l), ws₀), false)) + calc (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞)) + (ws₀.length : ℝ≥0∞) + = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞) + (ws₀.length : ℝ≥0∞)) := by + rw [show (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞) + (ws₀.length : ℝ≥0∞))) + = (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞)) + + ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * (ws₀.length : ℝ≥0∞) from by + rw [← ENNReal.tsum_add]; exact tsum_congr fun z => by rw [mul_add]] + rw [ENNReal.tsum_mul_right, hmass, one_mul] + _ = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (z.2.1.2.length : ℝ≥0∞) := by + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support run + · have hpre : ws₀.length ≤ z.2.1.2.length := + (deferredDraw_run_drawn_prefix ids M maxAttempts pk sk oa _ z hz).length_le + congr 1 + rw [← Nat.cast_add, Nat.sub_add_cancel hpre] + · rw [probOutput_eq_zero_of_not_mem_support hz, zero_mul, zero_mul] + _ ≤ (ws₀.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + have := deferredDraw_run_expected_length_le ids M maxAttempts pk sk hp₀ hp hAbort oa + qSrem hQ (((re, l), ws₀), false) + rwa [← hrun] at this + -- Cancel `ws₀.length` (finite) and rewrite `qSrem·ofReal(1/(1-p)) = ofReal(qSrem/(1-p))`. + have hcancel : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length - ws₀.length : ℕ) : ℝ≥0∞)) + ≤ (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [add_comm] at hsplit + exact (ENNReal.add_le_add_iff_left (by simp : (ws₀.length : ℝ≥0∞) ≠ ∞)).mp hsplit + refine hcancel.trans (le_of_eq ?_) + rw [← ENNReal.ofReal_natCast qSrem, ← ENNReal.ofReal_mul (by positivity)] + congr 1 + field_simp + +/-! ### Attempt-count law: the tight redraft of the deferral count + +The firing event tests reads against the *actual* (rejected) drawn list, whose draws are skewed by +the rejection conditioning `commit | reject`. The `drawList`-game RHS, by contrast, draws *raw* +`Prod.fst <$> ids.commit pk sk`. A reject-count `kn = drawnlist.length` is therefore *too small* to +dominate the firing (the residual was false-as-stated with that `kn`). The sound count is the total +*attempt* count, which over-counts each query's rejected draws by the accepting attempt's one fresh +raw draw and whose mean is exactly `qSrem/(1-p)`. + +The attempt count is recovered *without a new state field* as `(drawn-list growth) + (signed-list +growth)`: every signing query increments the signed-message list by exactly one (in +`deferredDrawImpl`'s sign branch) and the drawn list by its rejected-attempt count. Their sum +dominates the per-query attempt count and has the clean charge `∑_{a≤maxAttempts} p^a ≤ 1/(1-p)`, +combining the tight reject bound (`tsum_probOutput_run_ghostSignDrawBody_mul_length_le_tight`, the +`∑_{a (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hz + rw [support_map] at hzs + obtain ⟨u, _, rfl⟩ := hzs; rfl + · -- READ: writes only the base cache / bad flag; both lists preserved. + rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ ?_) + · intro z hz + have hzs : z ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, s.1.1.2), s.1.2), s.2 || decide (mc.2 ∈ s.1.2)))) <$> + roStep M s.1.1.1 mc) := hz + rw [support_map] at hzs + obtain ⟨cu, _, rfl⟩ := hzs; rfl + · simp only [deferredDrawImpl, StateT.run_mk] + rcases hg : s.1.1.1 mc with _ | v <;> simp [roStep, hg] + · -- SIGN: drawn list `s.1.2 ++ alc.1.2`, signed list `msg :: s.1.1.2` (one longer). + rw [if_pos (by simp)] + have hrun : (deferredDrawImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1 := rfl + rw [hrun] + refine le_of_eq_of_le (tsum_probOutput_map_mul + ((ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1) + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: s.1.1.2), s.1.2 ++ alc.1.2), s.2))) + (fun z => ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞))) ?_ + calc _ + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + (((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + (1 : ℝ≥0∞) + + (alc.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun alc => ?_ + simp only [List.length_append, List.length_cons] + push_cast + ring + _ = ((∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + (((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + (1 : ℝ≥0∞))) + + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1] * + (alc.1.2.length : ℝ≥0∞)) := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun alc => by rw [mul_add] + _ ≤ ((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [ENNReal.tsum_mul_right, tsum_probOutput_eq_one' (by simp), one_mul] + rw [add_assoc] + gcongr + refine le_trans (add_le_add_right + (tsum_probOutput_run_ghostSignDrawBody_mul_length_le_tight ids M pk sk msg + hAbort maxAttempts s.1.1.1) _) ?_ + rw [add_comm] + refine le_trans (le_of_eq ?_) (geomSum_le hp₀ hp (maxAttempts + 1)) + rw [Finset.sum_range_succ'] + simp only [pow_zero] + +omit [SampleableType Stmt] in +/-- **Run-level expected attempt count of the deferred-draw run.** By induction on `oa`, the +expected combined size `drawnlist.length + signedlist.length` of the deferred-draw run from a start +state `s` is at most `(s.1.2.length + s.1.1.2.length) + qSrem · (1/(1-p))`, where `qSrem` bounds the +number of signing queries. Each signing query grows the expected combined size by at most `1/(1-p)` +(the per-step charge `deferredDrawImpl_step_expected_attemptCount_le`), and uniform/read queries +leave it unchanged; the signing-query budget telescopes across the fold exactly as in +`deferredDraw_run_expected_length_le`. The attempt-count law `kn` of the redrafted residual inherits +this mean. -/ +theorem deferredDraw_run_expected_attemptCount_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qSrem : ℕ), oa.IsQueryBoundP (· matches Sum.inr _) qSrem → + ∀ (s : DeferredState M Commit Chal), + (∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s] * + ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + induction oa using OracleComp.inductionOn with + | pure a => + intro qSrem _ s + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + exact le_self_add + | query_bind t ob ih => + intro qSrem hQ s + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rw [simulateQ_query_bind, StateT.run_bind, tsum_probOutput_bind_mul] + set c : ℝ≥0∞ := ENNReal.ofReal (1 / (1 - p_abort)) with hc + have hmass : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredState M Commit Chal, + Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s]) = 1 := + tsum_probOutput_eq_one' (by + rcases t with (n | mc) | msg + · simp [deferredDrawImpl] + · simp only [deferredDrawImpl, StateT.run_mk] + rcases hg : s.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawImpl]) + have hfold : ∀ (b : ℕ) (extra : ℝ≥0∞), + (∀ x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredState M Commit Chal, + (∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c) → + (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + extra → + extra + (b : ℝ≥0∞) * c ≤ (qSrem : ℝ≥0∞) * c → + (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + intro b extra hcont hstep hbudget + calc (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + (((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c) := + ENNReal.tsum_le_tsum fun x => by gcongr; exact hcont x + _ = (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞)) + (b : ℝ≥0∞) * c := by + rw [show (∑' x, Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + (((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c)) + = ∑' x, (Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.2.length + x.2.1.1.2.length : ℕ) : ℝ≥0∞) + + Pr[= x | (deferredDrawImpl ids M maxAttempts pk sk t).run s] * + ((b : ℝ≥0∞) * c)) from tsum_congr fun x => by rw [mul_add]] + rw [ENNReal.tsum_add, ENNReal.tsum_mul_right, hmass, one_mul] + _ ≤ (((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + extra) + (b : ℝ≥0∞) * c := by gcongr + _ ≤ ((s.1.2.length + s.1.1.2.length : ℕ) : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + rw [add_assoc]; gcongr + rcases t with (n | mc) | msg + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawImpl_step_expected_attemptCount_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inl n)) s + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawImpl_step_expected_attemptCount_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inr mc)) s + · have hpos : 0 < qSrem := by + rcases hQ1 with hno | hpos + · exact absurd (by simp) hno + · exact hpos + refine hfold (qSrem - 1) c (fun x => ih x.1 (qSrem - 1) (by simpa using hQ2 x.1) x.2) ?_ ?_ + · have hstep := deferredDrawImpl_step_expected_attemptCount_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inr msg) s + rwa [if_pos (by rfl), ← hc] at hstep + · rw [add_comm, ← add_one_mul, + show ((qSrem - 1 : ℕ) : ℝ≥0∞) + 1 = (qSrem : ℝ≥0∞) by + have : qSrem - 1 + 1 = qSrem := by omega + rw [← this]; push_cast; ring] + +omit [SampleableType Stmt] in +/-- **The attempt-count law of the redrafted residual and its mean bound.** Mapping the +deferred-draw run to its attempt count — the combined new growth of the drawn and signed lists, +`(drawnlist.length - ws₀.length) + (signedlist.length - l.length)` — gives a count law whose mean is +at most `qSrem/(1-p)`. The attempt count dominates the reject count (it adds the signed-list growth, +one per signing query, covering each accepting attempt's fresh raw draw) yet keeps the same clean +mean, because the per-query charge `(reject expectation) + 1 = ∑_{a≤maxAttempts} p^a ≤ 1/(1-p)` is +identical to the loose reject charge — the `+1` is absorbed by tightening the reject bound from +`∑_{a + (z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length)) <$> + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run (((re, l), ws₀), false)] * + (n : ℝ≥0∞)) + ≤ ENNReal.ofReal ((qSrem : ℝ) / (1 - p_abort)) := by + classical + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + set run : ProbComp (γ × DeferredState M Commit Chal) := + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run (((re, l), ws₀), false) with hrun + have hmean : (∑' n : ℕ, Pr[= n | + (fun z : γ × DeferredState M Commit Chal => + (z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length)) <$> run] * + (n : ℝ≥0∞)) + = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) := + tsum_probOutput_map_mul run + (fun z => (z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length)) + (fun n => (n : ℝ≥0∞)) + rw [hmean] + -- Add back `ws₀.length + l.length` to recover the total combined size, bounded by the fold. + have hmass : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run]) = 1 := by + rw [hrun] + exact tsum_probOutput_eq_one' + (deferredDraw_run_neverFail ids M maxAttempts pk sk oa (((re, l), ws₀), false)) + have hsplit : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + + ((ws₀.length + l.length : ℕ) : ℝ≥0∞) + ≤ ((ws₀.length + l.length : ℕ) : ℝ≥0∞) + + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + calc (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + + ((ws₀.length + l.length : ℕ) : ℝ≥0∞) + = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) + + ((ws₀.length + l.length : ℕ) : ℝ≥0∞)) := by + rw [show (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) + + ((ws₀.length + l.length : ℕ) : ℝ≥0∞))) + = (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + + ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((ws₀.length + l.length : ℕ) : ℝ≥0∞) from by + rw [← ENNReal.tsum_add]; exact tsum_congr fun z => by rw [mul_add]] + rw [ENNReal.tsum_mul_right, hmass, one_mul] + _ = ∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + ((z.2.1.2.length + z.2.1.1.2.length : ℕ) : ℝ≥0∞) := by + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support run + · have hpre : ws₀.length ≤ z.2.1.2.length := + (deferredDraw_run_drawn_prefix ids M maxAttempts pk sk oa _ z hz).length_le + have hpre2 : l.length ≤ z.2.1.1.2.length := + deferredDraw_run_signed_prefix ids M maxAttempts pk sk oa _ z hz + congr 1 + rw [← Nat.cast_add] + congr 1 + omega + · rw [probOutput_eq_zero_of_not_mem_support hz, zero_mul, zero_mul] + _ ≤ ((ws₀.length + l.length : ℕ) : ℝ≥0∞) + + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + have := deferredDraw_run_expected_attemptCount_le ids M maxAttempts pk sk hp₀ hp hAbort + oa qSrem hQ (((re, l), ws₀), false) + rwa [← hrun] at this + have hcancel : (∑' z : γ × DeferredState M Commit Chal, Pr[= z | run] * + (((z.2.1.2.length - ws₀.length) + (z.2.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + ≤ (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [add_comm] at hsplit + exact (ENNReal.add_le_add_iff_left (by simp : ((ws₀.length + l.length : ℕ) : ℝ≥0∞) ≠ ∞)).mp + hsplit + refine hcancel.trans (le_of_eq ?_) + rw [← ENNReal.ofReal_natCast qSrem, ← ENNReal.ofReal_mul (by positivity)] + congr 1 + field_simp + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/GhostReadCharge.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/GhostReadCharge.lean new file mode 100644 index 000000000..fc5af96ee --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/GhostReadCharge.lean @@ -0,0 +1,494 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.BodyHops + +/-! +# EUF-CMA for Fiat-Shamir with aborts: GhostReadCharge + +The lazy-side ghost-read charge: query-bound transport across decidability +instances and the lazy ghost handler's per-read charge accounting. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ## The lazy-side ghost-read charge -/ + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +/-- Transport a predicate-targeted query bound across a (propositionally equal) choice of +predicate and `DecidablePred` instance. The decidability instance is a subsingleton up to +the propositional content; this lets a query bound built with one instance feed a lemma +expecting another (e.g. the accumulator's synthesised instance). -/ +lemma isQueryBoundP_cast_pred' {ι₀ : Type} {spec₀ : OracleSpec ι₀} {α₀ : Type} + {oa : OracleComp spec₀ α₀} {p₁ p₂ : spec₀.Domain → Prop} + {i₁ : DecidablePred p₁} {i₂ : DecidablePred p₂} {n : ℕ} (hp : p₁ = p₂) + (h : @OracleComp.IsQueryBoundP _ spec₀ α₀ oa p₁ i₁ n) : + @OracleComp.IsQueryBoundP _ spec₀ α₀ oa p₂ i₂ n := by + subst hp + rwa [Subsingleton.elim i₂ i₁] + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +/-- **Bad-flag pass-through for a bad-free run.** If every output of `oa` carries bad bit +`false`, then the bad probability of `oa >>= k` is carried entirely by `oa`'s (good) +outputs: it equals the resource-weighted sum the accumulator's free/charged step premises +require, with no extra bad mass introduced by `oa` itself. -/ +lemma probEvent_bad_bind_eq_tsum_false {γ' σ' δ' τ' : Type} + (oa : ProbComp (γ' × σ' × Bool)) + (k : γ' × σ' × Bool → ProbComp (δ' × τ' × Bool)) + (hbf : ∀ z ∈ support oa, z.2.2 = false) : + Pr[fun w => w.2.2 = true | oa >>= k] + = ∑' z : γ' × σ', + Pr[= (z.1, z.2, false) | oa] * Pr[fun w => w.2.2 = true | k (z.1, z.2, false)] := by + classical + rw [probEvent_bind_eq_tsum, + ← (Equiv.prodAssoc γ' σ' Bool).tsum_eq + (fun w => Pr[= w | oa] * Pr[fun y => y.2.2 = true | k w]), + ENNReal.tsum_prod'] + refine tsum_congr fun z => ?_ + rw [tsum_bool] + simp only [Equiv.prodAssoc_apply] + have htrue : Pr[= (z.1, z.2, true) | oa] = 0 := by + refine probOutput_eq_zero_of_not_mem_support fun hz => ?_ + exact absurd (hbf _ hz) (by simp) + rw [htrue, zero_mul, add_zero] + +omit [SampleableType Stmt] in +/-- **Charged-step premise for the lazy ghost read.** For the deferred-sampling handler +`lazyGhostHybridImpl`, an adversarial random-oracle read at `(.inl (.inr mc))` from a +non-bad state pays the amortizable flip charge `enncard (ghost cache) · ofReal ε` and +routes any residual bad mass through its `fired = false` (good) outputs. This is exactly +the `h_charged_step` hypothesis required by +`probEvent_bad_simulateQ_run_le_expectedQuerySlack`, made true (in contrast to the eager +handler's deterministic mass-`1` flip) by the lazy fire draw whose `true` mass is bounded +by `probOutput_lazyGhostFire_true_le_enncard`. -/ +lemma probEvent_lazyGhostHybridImpl_charged_step (pk : Stmt) (sk : Wit) {ε : ℝ} + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (mc : M × Commit) + (s : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) + (k : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (.inl (.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) × + Bool → + ProbComp ((M × Option (Commit × Resp)) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) × + Bool)) : + Pr[fun z => z.2.2 = true | + ((lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inr mc))).run (s, false)) >>= k] + ≤ QueryCache.enncard s.1.2 * ENNReal.ofReal ε + + ∑' z : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (.inl (.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (z.1, z.2, false) | + (lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inr mc))).run (s, false)] * + Pr[fun w => w.2.2 = true | k (z.1, z.2, false)] := by + classical + obtain ⟨⟨re, gh⟩, list⟩ := s + set fire := lazyGhostFire ids pk sk mc.2 gh.toSet.encard.toNat with hfire + set ro := roStep M re mc with hro + -- The lazy-fire `true`-mass is the amortizable flip charge `enncard gh · ofReal ε`. + have h_fire_true : Pr[= true | fire] ≤ QueryCache.enncard gh * ENNReal.ofReal ε := by + rw [hfire] + refine (probOutput_lazyGhostFire_true_le ids pk sk hGuess mc.2 _).trans ?_ + gcongr + -- `(encard.toNat : ℝ≥0∞) ≤ (encard : ℝ≥0∞) = enncard gh`. + change ((gh.toSet.encard.toNat : ℕ) : ℝ≥0∞) ≤ (gh.toSet.encard : ℝ≥0∞) + calc ((gh.toSet.encard.toNat : ℕ) : ℝ≥0∞) + = ((gh.toSet.encard.toNat : ℕ∞) : ℝ≥0∞) := by push_cast; rfl + _ ≤ (gh.toSet.encard : ℝ≥0∞) := ENat.toENNReal_mono (ENat.coe_toNat_le_self _) + -- The run, with its bad bit reduced (`false || b = b`): a fire draw whose Boolean result + -- becomes the output bad bit, composed with the real-layer caching read `ro`. + have h_run : (lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inr mc))).run + (((re, gh), list), false) = + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro := by + simp only [hfire, hro] + rfl + -- Rewrite both the run-bind and the good-continuation sum into the reduced form, then + -- expand the bad probability over each run output (`Chal × σ × Bool`). + rw [h_run, probEvent_bind_eq_tsum] + -- Unfold the `GhostState` abbreviation so the product structure is explicit. + simp only [GhostState] at * + -- Split each output sum over its Boolean (bad) coordinate. + rw [← (Equiv.prodAssoc + (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (Sum.inl (Sum.inr mc))) + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) + Bool).tsum_eq, + ENNReal.tsum_prod'] + -- The `bad = false` summand is the accumulator's good-continuation term; the `bad = true` + -- summand sums to the run's bad-output mass, bounded by `enncard gh · ofReal ε`. + have h_split : ∀ z : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inl (Sum.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + (∑' b : Bool, + Pr[= (z.1, z.2, b) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun y => y.2.2 = true | k (z.1, z.2, b)]) + = Pr[= (z.1, z.2, false) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun y => y.2.2 = true | k (z.1, z.2, false)] + + Pr[= (z.1, z.2, true) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun y => y.2.2 = true | k (z.1, z.2, true)] := by + intro z + rw [tsum_bool, add_comm] + simp only [Equiv.prodAssoc_apply] + -- Split each per-output Boolean sum into its `false` (good continuation) and `true` + -- (bad output) parts, then separate the two sums. + have hsplit_sum : + (∑' a : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inl (Sum.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + ∑' b : Bool, + Pr[= (a.1, a.2, b) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun z => z.2.2 = true | k (a.1, a.2, b)]) + = (∑' a : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inl (Sum.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (a.1, a.2, false) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun z => z.2.2 = true | k (a.1, a.2, false)]) + + (∑' a : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inl (Sum.inr mc))) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (a.1, a.2, true) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun z => z.2.2 = true | k (a.1, a.2, true)]) := by + rw [← ENNReal.tsum_add] + exact tsum_congr fun a => h_split a + refine le_trans (le_of_eq hsplit_sum) ?_ + rw [add_comm] + refine add_le_add ?_ le_rfl + -- The bad-output (`b = true`) mass is at most the fire `true`-mass: each output's bad bit + -- is the fire result, and the continuation contributes at most `1`. + calc (∑' z : Chal × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (z.1, z.2, true) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] * + Pr[fun y => y.2.2 = true | k (z.1, z.2, true)]) + ≤ ∑' z : Chal × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (z.1, z.2, true) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] := by + refine ENNReal.tsum_le_tsum fun z => ?_ + exact mul_le_of_le_one_right (zero_le) probEvent_le_one + _ ≤ Pr[= true | fire] := by + -- Each output's bad bit equals the fire draw, so the `b = true` outputs carry + -- at most the fire `true`-mass. Expand each summand over the fire draw, swap the + -- sums, and use that `g_fired <$> ro` outputs bad bit `fired`. + have h_per_z : ∀ z : Chal × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= (z.1, z.2, true) | + fire >>= fun fired => (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] + = ∑' fired : Bool, Pr[= fired | fire] * + Pr[= (z.1, z.2, true) | + (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro] := + fun z => probOutput_bind_eq_tsum fire _ _ + rw [tsum_congr h_per_z, ENNReal.tsum_comm] + -- The inner sum over outputs is `0` for `fired = false` (its outputs carry bad bit + -- `false`) and `≤ 1` for `fired = true`, giving the bound `≤ Pr[= true | fire]`. + have h_inner : ∀ fired : Bool, + (∑' z : Chal × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + Pr[= fired | fire] * + Pr[= (z.1, z.2, true) | + (fun cu => (cu.1, (((cu.2, gh), list), fired))) <$> ro]) + ≤ Pr[= fired | fire] * (if fired then 1 else 0) := by + intro fired + rw [ENNReal.tsum_mul_left] + gcongr ?_ * ?_ + · exact le_rfl + cases fired with + | false => + rw [if_neg (by decide)] + refine le_of_eq (ENNReal.tsum_eq_zero.mpr fun z => ?_) + refine probOutput_eq_zero_of_not_mem_support ?_ + rw [support_map] + rintro ⟨cu, _, heq⟩ + simp only [Prod.mk.injEq] at heq + exact absurd heq.2.2 (by decide) + | true => + rw [if_pos rfl] + -- The bad-output mass is a sub-sum of the total mass `≤ 1`, via the injection + -- `z ↦ (z.1, z.2, true)`. + refine le_trans (ENNReal.tsum_comp_le_tsum_of_injective ?_ + (fun w => Pr[= w | (fun cu => (cu.1, (((cu.2, gh), list), true))) <$> ro])) + tsum_probOutput_le_one + rintro ⟨a₁, b₁⟩ ⟨a₂, b₂⟩ heq + simp only [Prod.mk.injEq] at heq + exact Prod.ext heq.1 heq.2.1 + refine le_trans (ENNReal.tsum_le_tsum h_inner) ?_ + rw [tsum_bool] + simp + _ ≤ QueryCache.enncard gh * ENNReal.ofReal ε := h_fire_true + +omit [SampleableType Stmt] in +/-- A uniform-sampling read of the lazy ghost handler preserves the bad flag: started from a +non-bad state, every output is non-bad. -/ +lemma lazyGhostHybridImpl_run_unif_bad_false (pk : Stmt) (sk : Wit) (n : unifSpec.Domain) + (s : GhostState M Commit Chal) (hs : s.2 = false) : + ∀ z ∈ support ((lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inl n))).run s), + z.2.2 = false := by + intro z hz + rw [show (lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inl n))).run s = + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n from rfl] + at hz + obtain ⟨u, _, heq⟩ := + (support_map (fun u => (u, s)) ((HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) + ▸ hz) + rw [← heq, hs] + +omit [SampleableType Stmt] in +/-- A signing query of the lazy ghost handler preserves the bad flag: started from a non-bad +state, every output is non-bad. -/ +lemma lazyGhostHybridImpl_run_sign_bad_false (pk : Stmt) (sk : Wit) (msg : M) + (s : GhostState M Commit Chal) (hs : s.2 = false) : + ∀ z ∈ support ((lazyGhostHybridImpl ids M maxAttempts pk sk (.inr msg)).run s), + z.2.2 = false := by + intro z hz + rw [show (lazyGhostHybridImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc => (alc.1, ((alc.2, msg :: s.1.2), s.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run s.1.1 from rfl] at hz + obtain ⟨alc, _, heq⟩ := + (support_map (fun alc : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) => + (alc.1, ((alc.2, msg :: s.1.2), s.2))) + ((ghostSignBody ids M pk sk msg maxAttempts).run s.1.1) ▸ hz) + rw [← heq, hs] + +omit [SampleableType Stmt] in +/-- **Deliverable A: the lazy-side ghost-read bound.** For the deferred-sampling handler +`lazyGhostHybridImpl`, the probability that the adversary's run ever flips the bad flag is +at most `qS·(qH+1)·ε/(1-p)`. + +Assembled from the single-world resource-charged accumulator +`probEvent_bad_simulateQ_run_le_expectedQuerySlack` (charged step = +`probEvent_lazyGhostHybridImpl_charged_step`, free step = the bad-flag pass-through of +non-read queries) chained with the charged-read / expected-growth fold +`expectedQuerySlack_charged_read_expected_growth_le` (resource `R s := enncard (ghost +cache)`, per-read charge `ofReal ε`, expected growth `g := ∑_{a ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (lazyGhostHybridImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) := by + classical + -- The assembly, in three stages, executed below. + -- + -- (1) Single-world accumulator + -- `OracleComp.ProgramLogic.Relational.probEvent_bad_simulateQ_run_le_expectedQuerySlack` + -- at `impl := lazyGhostHybridImpl ids M maxAttempts pk sk`, + -- `charged := (· matches Sum.inl (Sum.inr _))` (random-oracle reads), + -- `R s := QueryCache.enncard s.1.2` (the ghost cache size), `ε := ofReal ε`, with + -- * `h_charged_step := probEvent_lazyGhostHybridImpl_charged_step …` (PROVEN above); + -- * `h_free_step` from the bad-flag pass-through `probEvent_bad_bind_eq_tsum_false` + -- combined with `lazyGhostHybridImpl_run_unif_bad_false` / + -- `lazyGhostHybridImpl_run_sign_bad_false` (all PROVEN above); + -- * charged-read budget `qH + 1` from `(hQ pk).2.mono` transported across the + -- `DecidablePred` instance by `isQueryBoundP_cast_pred'` (PROVEN above). + -- This yields + -- `Pr[bad | run] ≤ expectedQuerySlack lazyGhostHybridImpl charged + -- (fun s => R s * ofReal ε) (adv.main pk) (qH+1) (init, false)`. + -- + -- (2) The charged-read / expected-growth fold + -- `OracleComp.ProgramLogic.Relational.expectedQuerySlack_charged_read_expected_growth_le` + -- with `chargedQuery := reads`, `growthQuery := (· matches Sum.inr _)` (signings), + -- `R`, `β := ofReal ε`, `g := ∑_{a t matches Sum.inl (Sum.inr _)) + (R := fun s => QueryCache.enncard s.1.2) (ε := ENNReal.ofReal ε) + ?_ ?_ (adv.main pk) (qS := qH + 1) ?_ (((∅, ∅), []))).trans ?_ + · -- h_charged_step: a charged random-oracle read pays `enncard · ofReal ε`. + rintro t s ht k + obtain ⟨mc, rfl⟩ : ∃ mc, t = Sum.inl (Sum.inr mc) := by + revert ht; rcases t with (n | mc) | msg <;> simp + exact probEvent_lazyGhostHybridImpl_charged_step ids M maxAttempts pk sk hGuess mc s k + · -- h_free_step: a non-charged (uniform or signing) query introduces no bad mass. + rintro t s ht k + rcases t with (n | mc) | msg + · exact le_of_eq (probEvent_bad_bind_eq_tsum_false + (oa := (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run (s, false)) + (k := k) + (lazyGhostHybridImpl_run_unif_bad_false ids M maxAttempts pk sk n (s, false) rfl)) + · exact absurd rfl ht + · exact le_of_eq (probEvent_bad_bind_eq_tsum_false + (oa := (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inr msg)).run (s, false)) + (k := k) + (lazyGhostHybridImpl_run_sign_bad_false ids M maxAttempts pk sk msg (s, false) rfl)) + · -- charged-read budget `qH + 1`: from the RO-read bound `qH`, weakened by `+1`. + have h := (hQ pk).2.mono (Nat.le_succ qH) + convert h using 3 with x + rcases x with (_ | _) | _ <;> rfl + · -- (2)+(3): the charged-read / expected-growth fold, then arithmetic. + set g : ℝ≥0∞ := ∑ a ∈ Finset.range maxAttempts, ENNReal.ofReal p_abort ^ a with hg + -- The fold bound: `expectedQuerySlack ≤ (qH+1)·(R init + qS·g)·ofReal ε`. + have h_fold : + OracleComp.ProgramLogic.Relational.expectedQuerySlack + (lazyGhostHybridImpl ids M maxAttempts pk sk) + (fun t => t matches Sum.inl (Sum.inr _)) + (fun s => QueryCache.enncard s.1.2 * ENNReal.ofReal ε) (adv.main pk) (qH + 1) + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M), + false) + ≤ ((qH + 1 : ℕ) : ℝ≥0∞) * + (QueryCache.enncard (∅ : (M × Commit →ₒ Chal).QueryCache) + (qS : ℝ≥0∞) * g) * + ENNReal.ofReal ε := by + refine OracleComp.ProgramLogic.Relational.expectedQuerySlack_charged_read_expected_growth_le + (lazyGhostHybridImpl ids M maxAttempts pk sk) + (chargedQuery := fun t => t matches Sum.inl (Sum.inr _)) + (growthQuery := fun t => t matches Sum.inr _) + (R := fun s => QueryCache.enncard s.1.2) (β := ENNReal.ofReal ε) (g := g) + ?_ ?_ ?_ (adv.main pk) ?_ ?_ _ + · -- h_charged: a charged RO read leaves the ghost cache (`R`) unchanged. + rintro t p hp ht z hz + rcases t with (n | mc) | msg + · exact absurd ht (by simp) + · rw [show (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inl (Sum.inr mc))).run p = + lazyGhostFire ids pk sk mc.2 p.1.1.2.toSet.encard.toNat >>= fun fired => + (fun cu => (cu.1, (((cu.2, p.1.1.2), p.1.2), p.2 || fired))) <$> + roStep M p.1.1.1 mc from rfl] at hz + obtain ⟨fired, _, hz⟩ := (mem_support_bind_iff _ _ _).1 hz + obtain ⟨cu, _, heq⟩ := + (support_map (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, p.1.1.2), p.1.2), p.2 || fired))) + (roStep M p.1.1.1 mc) ▸ hz) + rw [← heq] + · exact absurd ht (by simp) + · -- h_growth: the ghost-layer growth law for a signing query. + rintro t p hp ht ht2 + rcases t with (n | mc) | msg + · exact absurd ht2 (by simp) + · exact absurd ht2 (by simp) + · obtain ⟨⟨⟨re, gh⟩, list⟩, b⟩ := p + rw [show b = false from hp] + change ∑' z : (Option (Commit × Resp)) × + (((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × List M) × Bool, + Pr[= z | (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inr msg)).run + (((re, gh), list), false)] * QueryCache.enncard z.2.1.1.2 + ≤ QueryCache.enncard gh + g + rw [show (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inr msg)).run + (((re, gh), list), false) = + (ghostSignBody ids M pk sk msg maxAttempts).run (re, gh) >>= fun alc => + pure (alc.1, ((alc.2, msg :: list), false)) from by rw [bind_pure_comp]; rfl] + have heq := tsum_probOutput_bind_mul + ((ghostSignBody ids M pk sk msg maxAttempts).run (re, gh)) + (fun alc => (pure (alc.1, ((alc.2, msg :: list), false)) : ProbComp _)) + (fun z => QueryCache.enncard z.2.1.1.2) + refine le_trans (le_of_eq heq) ?_ + refine le_trans (ENNReal.tsum_le_tsum fun alc => ?_) + (tsum_probOutput_run_ghostSignBody_mul_ghost_enncard_le ids M pk sk msg hAbort + maxAttempts re gh) + rw [tsum_probOutput_pure_mul] + · -- h_free: a uniform query leaves the ghost cache (`R`) unchanged. + rintro t p hp ht ht2 z hz + rcases t with (n | mc) | msg + · rw [show (lazyGhostHybridImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run p = + (fun u => (u, p)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n from rfl] at hz + obtain ⟨u, _, heq⟩ := + (support_map (fun u => (u, p)) + ((HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) ▸ hz) + rw [← heq] + · exact absurd ht (by simp) + · exact absurd ht2 (by simp) + · -- charged budget `qH + 1`. + have h := (hQ pk).2.mono (Nat.le_succ qH) + convert h using 3 with x + rcases x with (_ | _) | _ <;> rfl + · -- growth budget `qS`. + have h := (hQ pk).1 + convert h using 3 with x + rcases x with (_ | _) | _ <;> rfl + refine h_fold.trans ?_ + -- (3) Arithmetic: `enncard ∅ = 0`, `g = ofReal S` with `S = ∑ pᵃ ≤ 1/(1-p)`. + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + set S : ℝ := ∑ a ∈ Finset.range maxAttempts, p_abort ^ a with hSdef + have hSnn : 0 ≤ S := Finset.sum_nonneg fun a _ => pow_nonneg hp₀ a + have hg_eq : g = ENNReal.ofReal S := by + rw [hg, hSdef, ENNReal.ofReal_sum_of_nonneg (fun a _ => pow_nonneg hp₀ a)] + exact Finset.sum_congr rfl fun a _ => by rw [← ENNReal.ofReal_pow hp₀] + have hSgeo : S ≤ 1 / (1 - p_abort) := by + rw [hSdef, le_div_iff₀ h1p] + have hmul := geom_sum_mul p_abort maxAttempts + nlinarith [pow_nonneg hp₀ maxAttempts] + rw [QueryCache.enncard_empty, zero_add, hg_eq, + show ((qH + 1 : ℕ) : ℝ≥0∞) = ENNReal.ofReal ((qH : ℝ) + 1) from by + rw [← ENNReal.ofReal_natCast (qH + 1)]; push_cast; ring_nf, + show (qS : ℝ≥0∞) = ENNReal.ofReal qS from (ENNReal.ofReal_natCast qS).symm] + rw [← ENNReal.ofReal_mul (by positivity), ← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_mul (by positivity)] + refine ENNReal.ofReal_le_ofReal ?_ + have hqS : (0 : ℝ) ≤ qS := Nat.cast_nonneg qS + have hqH1 : (0 : ℝ) ≤ (qH : ℝ) + 1 := by positivity + calc ((qH : ℝ) + 1) * (qS * S) * ε + ≤ ((qH : ℝ) + 1) * (qS * (1 / (1 - p_abort))) * ε := by + gcongr + _ = qS * ((qH : ℝ) + 1) * ε / (1 - p_abort) := by ring + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HiddenReadFold.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HiddenReadFold.lean new file mode 100644 index 000000000..90938d5b6 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HiddenReadFold.lean @@ -0,0 +1,553 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.GhostReadCharge + +/-! +# EUF-CMA for Fiat-Shamir with aborts: HiddenReadFold + +The direct route for the eager ghost-read bad probability: the averaged +multi-key hidden-read fold `hiddenReadList_fold_le_target` reducing to the +multi-key first-fire bound, and the per-query eager↔lazy deferred-sampling coupling +at the ghost-read leaf, with its bookkeeping support lemmas. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ## Direct route: averaged multi-key hidden-read fold to the target + +The direct route bounds the eager ghost-read bad probability by the multi-key +hidden-target first-fire bound `OracleComp.probEvent_hiddenReadList_le` (`≤ n·(qH+1)·ε` +for `n` ghost keys), then averages the per-key-count bound over the run's key-count law. +The averaging step is the pure-`ℝ≥0∞` arithmetic fold `hiddenReadList_fold_le_target` +below: it takes any sub-probability weight `P : ℕ → ℝ≥0∞` over the number of ghost keys +whose mean is bounded by the expected attempt count `qS/(1-p)` (the +`tsum_probOutput_commit_mul_abort_le` aggregate) and folds the per-count bound +`k·(qH+1)·ε` into the target `qS·(qH+1)·ε/(1-p)`. It is the `[fold]` step of the chain +`Pr[eager bad] ≤ Pr[readManyList …] = Pr[hiddenReadList …] ≤ n·(qH+1)·ε ≤[fold] target`, +consumed by `probEvent_ghostBlind_bad_le_of_fac` once that chain's deferred-sampling +factorization is supplied as the hypothesis `hfac`. The headline instead charges the +ghost-read bound through the first-moment route of +`Security/TapeFactorization.lean`. -/ +lemma hiddenReadList_fold_le_target (qS qH : ℕ) (ε p_abort : ℝ) (hp : p_abort < 1) + (P : ℕ → ℝ≥0∞) + (hmean : ∑' k : ℕ, P k * (k : ℝ≥0∞) ≤ ENNReal.ofReal (qS / (1 - p_abort))) : + (∑' k : ℕ, P k * ((k : ℝ≥0∞) * (((qH : ℝ≥0∞) + 1) * ENNReal.ofReal ε))) + ≤ ENNReal.ofReal (qS * ((qH : ℝ) + 1) * ε / (1 - p_abort)) := by + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + have hqH1 : ((qH : ℝ≥0∞) + 1) = ENNReal.ofReal ((qH : ℝ) + 1) := by + rw [← ENNReal.ofReal_natCast qH, ← ENNReal.ofReal_one, + ← ENNReal.ofReal_add (by positivity) (by norm_num)] + have h1 : (∑' k : ℕ, P k * ((k : ℝ≥0∞) * (((qH : ℝ≥0∞) + 1) * ENNReal.ofReal ε))) + = (∑' k : ℕ, P k * (k : ℝ≥0∞)) * (ENNReal.ofReal ((qH : ℝ) + 1) * ENNReal.ofReal ε) := by + rw [← ENNReal.tsum_mul_right]; congr 1; ext k; rw [hqH1]; ring + rw [h1, ← ENNReal.ofReal_mul (by positivity)] + calc (∑' k : ℕ, P k * (k : ℝ≥0∞)) * ENNReal.ofReal (((qH : ℝ) + 1) * ε) + ≤ ENNReal.ofReal (qS / (1 - p_abort)) * ENNReal.ofReal (((qH : ℝ) + 1) * ε) := by gcongr + _ = ENNReal.ofReal (qS / (1 - p_abort) * (((qH : ℝ) + 1) * ε)) := by + rw [← ENNReal.ofReal_mul (by positivity)] + _ ≤ ENNReal.ofReal (qS * ((qH : ℝ) + 1) * ε / (1 - p_abort)) := by + apply ENNReal.ofReal_le_ofReal; apply le_of_eq; field_simp + +omit [SampleableType Stmt] in +/-- **(c) Expected-attempt geometric fold.** The per-signing-query attempt-count mass +`∑_{a pow_nonneg hp₀ a)] + exact Finset.sum_congr rfl fun a _ => by rw [← ENNReal.ofReal_pow hp₀] + rw [hg_eq] + refine ENNReal.ofReal_le_ofReal ?_ + rw [hSdef, le_div_iff₀ h1p] + have hmul := geom_sum_mul p_abort maxAttempts + nlinarith [pow_nonneg hp₀ maxAttempts] + +omit [SampleableType Stmt] in +/-- **General geometric attempt-count fold.** For any number of terms `n`, the geometric sum +`∑_{a pow_nonneg hp₀ a)] + exact Finset.sum_congr rfl fun a _ => by rw [← ENNReal.ofReal_pow hp₀] + rw [hg_eq] + refine ENNReal.ofReal_le_ofReal ?_ + rw [le_div_iff₀ h1p] + have hmul := geom_sum_mul p_abort n + nlinarith [pow_nonneg hp₀ n] + +/-! ## Deferred-sampling eager↔lazy coupling (ghost-read leaf) -/ + +omit [SampleableType Stmt] in +/-- **Uniform-branch per-query coupling for the eager↔lazy ghost handlers.** On a +uniform query both `ghostHybridImpl … true` and `lazyGhostHybridImpl` forward the draw and +leave the state untouched (`lazyGhostHybridImpl_run_unif_eq`), so they are coupled by the +identity coupling on the shared uniform sample with *equal outputs* and the bad-flag +implication preserved verbatim. This is the divergence-free branch of `h_step`. -/ +theorem relTriple_ghostHybrid_lazyGhost_unif (pk : Stmt) (sk : Wit) + (n : unifSpec.Domain) (e l : GhostState M Commit Chal) (hRel : e.2 = true → l.2 = true) : + OracleComp.ProgramLogic.Relational.RelTriple + ((ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inl n))).run e) + ((lazyGhostHybridImpl ids M maxAttempts pk sk (.inl (.inl n))).run l) + (fun p₁ p₂ => p₁.1 = p₂.1 ∧ (p₁.2.2 = true → p₂.2.2 = true)) := by + classical + rw [lazyGhostHybridImpl_run_unif_eq ids M maxAttempts pk sk n l] + simp only [ghostHybridImpl, StateT.run_mk, map_eq_bind_pure_comp] + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (OracleComp.ProgramLogic.Relational.relTriple_refl + ((HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n)) ?_ + rintro u u' rfl + exact OracleComp.ProgramLogic.Relational.relTriple_pure_pure ⟨rfl, hRel⟩ + +omit [SampleableType Stmt] in +/-- **Signing-branch per-query coupling for the eager↔lazy ghost handlers.** On a +signing query both handlers run the *same* `ghostSignBody` over the layered cache, prepend +`msg` to the signed-message list, and leave the bad flag untouched +(`lazyGhostHybridImpl_run_sign_eq`); they are therefore identical, so coupled by the +identity coupling with equal outputs and the bad-flag implication preserved. This is the +second divergence-free branch of `h_step`. -/ +theorem relTriple_ghostHybrid_lazyGhost_sign (pk : Stmt) (sk : Wit) + (msg : M) (e l : GhostState M Commit Chal) (hRel : e.2 = true → l.2 = true) : + OracleComp.ProgramLogic.Relational.RelTriple + ((ghostHybridImpl ids M maxAttempts true pk sk (.inr msg)).run e) + ((lazyGhostHybridImpl ids M maxAttempts pk sk (.inr msg)).run l) + (fun p₁ p₂ => p₁.2.2 = true → p₂.2.2 = true) := by + classical + -- The signing handlers copy the input bad flag to the output (`alc ↦ (…, s.2)`), so the + -- output bad flag is the *constant* `e.2` on the left and `l.2` on the right, independent of + -- the `ghostSignBody` draw. Couple the two (possibly differently-cached) `ghostSignBody` + -- runs by *any* coupling (the product coupling from `relTriple_true`), then map both to + -- `pure`s whose bad flags are `e.2` / `l.2`; the post is then exactly `hRel`. + rw [lazyGhostHybridImpl_run_sign_eq ids M maxAttempts pk sk msg l] + simp only [ghostHybridImpl, StateT.run_mk, map_eq_bind_pure_comp] + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (OracleComp.ProgramLogic.Relational.relTriple_true + ((ghostSignBody ids M pk sk msg maxAttempts).run e.1.1) + ((ghostSignBody ids M pk sk msg maxAttempts).run l.1.1)) ?_ + rintro a b - + exact OracleComp.ProgramLogic.Relational.relTriple_pure_pure hRel + +/-! ## Measure-level eager↔lazy coupling: support lemmas + +The lemmas in this section supply bookkeeping used by `avgBadM_eager_le_lazy_joint` +(the reusable two-measure coupling engine of `Security/CouplingEngine.lean`). Both +handlers agree on uniform and signing steps (`relTriple_ghostHybrid_lazyGhost_unif` / +`relTriple_ghostHybrid_lazyGhost_sign`), so the per-step premises concern the per-step +invariant-preservation at those steps: expected ghost-cache size, flag-preservation, +charge-carry, and charge-K bookkeeping. For the uniform and signing branches both handlers +are definitionally identical (`lazyGhostHybridImpl_run_unif_eq` / +`lazyGhostHybridImpl_run_sign_eq`), so the post-step measures agree and any coupling +invariant is threaded unchanged. -/ + +open scoped Classical in +/-- **Uniform-handler pushforward identity (inert plumbing).** The uniform branch of +`ghostHybridImpl` is the state-fixing pushforward `(fun u => (u, p)) <$> oa` of the uniform +draw `oa`. Averaging a functional `F` over the post-step `(output, state)` pair therefore +collapses the state coordinate to the fixed `p`: the per-`p` inner sum equals the plain +uniform average of `F (·, p)`. Pure measure-theoretic rearrangement (`ENNReal.tsum_prod'`, +off-diagonal collapse, `probOutput_map_injective` on the injective `(·, p)`); no +probabilistic content. -/ +lemma tsum_probOutput_map_state_fixed {R G : Type} (oa : ProbComp R) (p : G) + (F : R × G → ℝ≥0∞) : + (∑' z : R × G, Pr[= z | (fun u => (u, p)) <$> oa] * F z) + = ∑' u : R, Pr[= u | oa] * F (u, p) := by + classical + rw [ENNReal.tsum_prod'] + refine tsum_congr fun u => ?_ + rw [tsum_eq_single p ?_] + · rw [probOutput_map_injective oa (f := fun u => (u, p)) + (fun a b h => (Prod.ext_iff.mp h).1) u] + · intro g hg + rw [probOutput_eq_zero_of_not_mem_support, zero_mul] + intro hmem + rw [support_map] at hmem + obtain ⟨u', _, hu'⟩ := hmem + exact hg (Prod.ext_iff.mp hu').2.symm + +omit [SampleableType Stmt] in +/-- Uniform step preserves the per-state expected ghost size: the handler returns `(u, p)` +fixing the state, so the post-step ghost layer is always `p`'s. -/ +lemma ghostHybridImpl_unif_expected_enncard (pk : Stmt) (sk : Wit) + (n : unifSpec.Domain) (p : GhostState M Commit Chal) : + (∑' z : unifSpec.Range n × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inl n))).run p] * + QueryCache.enncard (z.2.1.1.2)) + = QueryCache.enncard (p.1.1.2) := by + classical + calc (∑' z : unifSpec.Range n × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inl n))).run p] * + QueryCache.enncard (z.2.1.1.2)) + = ∑' u : unifSpec.Range n, + Pr[= u | (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n] * + QueryCache.enncard (p.1.1.2) := + tsum_probOutput_map_state_fixed + ((HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) p + (fun z => QueryCache.enncard (z.2.1.1.2)) + _ = QueryCache.enncard (p.1.1.2) := by + rw [ENNReal.tsum_mul_right, tsum_probOutput_eq_one' (by simp), one_mul] + +omit [SampleableType Stmt] in +/-- Read step preserves the per-state expected ghost size: the eager read writes only the +*base* cache layer (or flips the bad flag), never the ghost layer. -/ +lemma ghostHybridImpl_read_expected_enncard (pk : Stmt) (sk : Wit) + (mc : M × Commit) (p : GhostState M Commit Chal) : + (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + QueryCache.enncard (z.2.1.1.2)) + = QueryCache.enncard (p.1.1.2) := by + classical + have hghost : ∀ z : Chal × GhostState M Commit Chal, + z ∈ support ((ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p) → + z.2.1.1.2 = p.1.1.2 := by + intro z hz + simp only [ghostHybridImpl, StateT.run_mk] at hz + rcases hgh : p.1.1.2 mc with _ | v + · simp only [hgh, support_map] at hz + obtain ⟨cu, _, rfl⟩ := hz; rfl + · simp only [hgh, ↓reduceIte, support_pure] at hz + subst hz; rfl + have hconst : (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + QueryCache.enncard (z.2.1.1.2)) + = ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + QueryCache.enncard (p.1.1.2) := by + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support ((ghostHybridImpl ids M maxAttempts true pk sk + (.inl (.inr mc))).run p) + · congr 1 + exact congrArg QueryCache.enncard (hghost z hz) + · have h0 : Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] + = 0 := probOutput_eq_zero_of_not_mem_support hz + rw [h0, zero_mul, zero_mul] + rw [hconst, ENNReal.tsum_mul_right] + have hone : (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p]) = 1 := by + refine tsum_probOutput_eq_one' ?_ + simp only [ghostHybridImpl, StateT.run_mk] + rcases hgh : p.1.1.2 mc with _ | v + · rcases p.1.1.1 mc with _ | v' <;> simp [roStep] + · simp + rw [hone, one_mul] + +omit [SampleableType Stmt] in +/-- Sign step grows the per-state expected ghost size by at most `∑ attempts ≤ 1/(1-p)`: the +signing body's accepted-transcript / rejected-attempt programming writes to the ghost layer +(`tsum_probOutput_run_ghostSignBody_mul_ghost_enncard_le` plus the geometric fold). -/ +lemma ghostHybridImpl_sign_expected_enncard_le (pk : Stmt) (sk : Wit) (msg : M) + {p_abort : ℝ} + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (p : GhostState M Commit Chal) : + (∑' z : Option (Commit × Resp) × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inr msg)).run p] * + QueryCache.enncard (z.2.1.1.2)) + ≤ QueryCache.enncard (p.1.1.2) + ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + -- The handler maps the `ghostSignBody` output state into the ghost layer; the expected + -- ghost size of the result equals that of the `ghostSignBody` output's ghost component. + have hmap : (∑' z : Option (Commit × Resp) × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inr msg)).run p] * + QueryCache.enncard (z.2.1.1.2)) + = ∑' w : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache), + Pr[= w | (ghostSignBody ids M pk sk msg maxAttempts).run p.1.1] * + QueryCache.enncard (w.2.2) := by + rw [show (ghostHybridImpl ids M maxAttempts true pk sk (.inr msg)).run p + = (fun alc => (alc.1, ((alc.2, msg :: p.1.2), p.2))) <$> + (ghostSignBody ids M pk sk msg maxAttempts).run p.1.1 from rfl] + -- Reindex the post-step sum over the injective map `alc ↦ (alc.1, ((alc.2, …), …))`. + set g : Option (Commit × Resp) × + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) → + Option (Commit × Resp) × GhostState M Commit Chal := + fun alc => (alc.1, ((alc.2, msg :: p.1.2), p.2)) with hg + have hginj : Function.Injective g := by + intro a b hab + simp only [hg, Prod.mk.injEq] at hab + exact Prod.ext hab.1 hab.2.1.1 + refine tsum_eq_tsum_of_ne_zero_bij (fun w => g w.1) ?_ ?_ ?_ + · intro a b hab + exact Subtype.ext (hginj hab) + · intro z hz + simp only [Function.mem_support, ne_eq, mul_eq_zero, not_or] at hz + have hzs : z ∈ support (g <$> (ghostSignBody ids M pk sk msg maxAttempts).run p.1.1) := + (mem_support_iff _ z).mpr hz.1 + rw [support_map] at hzs + obtain ⟨w, hw, rfl⟩ := hzs + refine ⟨⟨w, ?_⟩, rfl⟩ + simp only [Function.mem_support, ne_eq, mul_eq_zero, not_or] + refine ⟨probOutput_ne_zero_of_mem_support hw, ?_⟩ + have heq : ((g w).2.1.1.2) = w.2.2 := rfl + rw [heq] at hz; exact hz.2 + · rintro ⟨w, hw⟩ + change Pr[= g w | g <$> (ghostSignBody ids M pk sk msg maxAttempts).run p.1.1] * + QueryCache.enncard ((g w).2.1.1.2) + = Pr[= w | (ghostSignBody ids M pk sk msg maxAttempts).run p.1.1] * + QueryCache.enncard (w.2.2) + rw [probOutput_map_injective _ hginj] + rw [hmap] + refine le_trans (tsum_probOutput_run_ghostSignBody_mul_ghost_enncard_le ids M pk sk msg + hAbort maxAttempts p.1.1.1 p.1.1.2) ?_ + gcongr + exact geomAttemptSum_le maxAttempts hp₀ hp + +/-- **Per-state ghost charge accumulator** for the threaded eager-charge bound: the +mass-weighted total size of the ghost cache layer. Linear in the state measure `ν`, preserved +by read/uniform steps (which never write the ghost layer) and grown additively by sign steps +(`tsum_probOutput_run_ghostSignBody_mul_ghost_enncard_le`). -/ +noncomputable def ghostChargeK (ν : GhostState M Commit Chal → ℝ≥0∞) : ℝ≥0∞ := + ∑' p : GhostState M Commit Chal, ν p * QueryCache.enncard (p.1.1.2) + +/-- **Averaged ghost-membership charge invariant.** For every read target `mc`, the +`ν`-averaged membership charge at `mc` is dominated by the ghost-size accumulator scaled by +`ofReal ε`. This is the carried invariant of the threaded eager-charge bound: it holds at the +empty-cache Dirac start (`0 ≤ 0`), is preserved by reads (ghost layer untouched) and signs +(a sign step raises the charge by `≤ (attempts)·ε`, matching the enncard growth of +`ghostChargeK` — `ghostHybridImpl_sign_expected_enncard_le`). It is only an *averaged* +fact — pointwise per state it is false, since a single ghost entry costs `1`, not `ε`. -/ +def ghostChargeInv (ε : ℝ) (ν : GhostState M Commit Chal → ℝ≥0∞) : Prop := + ∀ mc : M × Commit, + (∑' p : GhostState M Commit Chal, ν p * memCharge M p.1.1.2 mc) + ≤ ghostChargeK M ν * ENNReal.ofReal ε + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +/-- A step that never writes the ghost layer and preserves the bad flag (uniform forward, or a +signing step — whose handler leaves `s.2` untouched) preserves the per-state expected bad +mass: the post-step flag equals the pre-step flag with probability one (mass `≤ 1`). -/ +lemma ghostHybridImpl_flag_preserved_le {γ : Type} + (run : ProbComp (γ × GhostState M Commit Chal)) (p : GhostState M Commit Chal) + (hflag : ∀ z ∈ support run, z.2.2 = p.2) : + (∑' z : γ × GhostState M Commit Chal, Pr[= z | run] * (if z.2.2 = true then 1 else 0)) + ≤ (if p.2 = true then 1 else 0) := by + classical + calc (∑' z : γ × GhostState M Commit Chal, Pr[= z | run] * (if z.2.2 = true then 1 else 0)) + = ∑' z : γ × GhostState M Commit Chal, Pr[= z | run] * (if p.2 = true then 1 else 0) := by + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support run + · rw [hflag z hz] + · rw [probOutput_eq_zero_of_not_mem_support hz, zero_mul, zero_mul] + _ = (∑' z : γ × GhostState M Commit Chal, Pr[= z | run]) * (if p.2 = true then 1 else 0) := by + rw [ENNReal.tsum_mul_right] + _ ≤ (if p.2 = true then 1 else 0) := + mul_le_of_le_one_left zero_le tsum_probOutput_le_one + +omit [SampleableType Stmt] in +/-- Per-state read-step bad-mass bound: the eager read sets the bad flag only on a ghost hit, +so the expected post-step bad mass is at most the pre-step flag plus the membership charge of +the read target. -/ +lemma ghostHybridImpl_read_expected_flag_le (pk : Stmt) (sk : Wit) + (mc : M × Commit) (p : GhostState M Commit Chal) : + (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + (if z.2.2 = true then 1 else 0)) + ≤ (if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc := by + classical + -- On a miss the post-step flag is preserved; on a hit it is forced true. In both cases the + -- post-step flag is `≤ p.2 ∨ (ghost hit at mc)` — captured by `memCharge`. + have hflag : ∀ z ∈ support ((ghostHybridImpl ids M maxAttempts true pk sk + (.inl (.inr mc))).run p), + (if z.2.2 = true then (1 : ℝ≥0∞) else 0) + ≤ (if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc := by + intro z hz + simp only [ghostHybridImpl, StateT.run_mk] at hz + rcases hgh : p.1.1.2 mc with _ | v + · -- Miss: flag preserved, `memCharge = 0`. + simp only [hgh, support_map] at hz + obtain ⟨cu, -, rfl⟩ := hz + exact le_add_right le_rfl + · -- Hit: flag forced true, `memCharge = 1`. + simp only [hgh, ↓reduceIte, support_pure, Set.mem_singleton_iff] at hz + subst hz + rw [show memCharge M p.1.1.2 mc = 1 by simp [memCharge, hgh]] + exact le_add_self + calc (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + (if z.2.2 = true then 1 else 0)) + ≤ ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + ((if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc) := by + refine ENNReal.tsum_le_tsum fun z => ?_ + by_cases hz : z ∈ support ((ghostHybridImpl ids M maxAttempts true pk sk + (.inl (.inr mc))).run p) + · gcongr; exact hflag z hz + · refine le_of_eq (mul_eq_zero.mpr (Or.inl ?_)) |>.trans zero_le + exact probOutput_eq_zero_of_not_mem_support hz + _ = (∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p]) * + ((if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc) := by + rw [ENNReal.tsum_mul_right] + _ ≤ (if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc := + mul_le_of_le_one_left zero_le tsum_probOutput_le_one + +omit [SampleableType Stmt] in +/-- **`h_carry` premise of the threaded bound for the ghost handler.** The carried bad mass +telescopes across one step, paying the read hit charge `≤ K ν · ofReal ε` on a read step +(via the invariant `ghostChargeInv`). Uniform/sign steps preserve the carried bad mass. -/ +lemma avgBadM_ghostHybridImpl_threaded_carry + (ε p_abort : ℝ) (_hp₀ : 0 ≤ p_abort) (_hp : p_abort < 1) (_hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (_hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (_hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (ν : GhostState M Commit Chal → ℝ≥0∞) + (_hInv : ghostChargeInv M ε ν) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) : + (∑' u : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t, + ∑' p : GhostState M Commit Chal, + OracleComp.ProgramLogic.Relational.postStepOutM + (ghostHybridImpl ids M maxAttempts true pk sk) ν t u p * + (if p.2 = true then 1 else 0)) + ≤ (∑' p : GhostState M Commit Chal, ν p * (if p.2 = true then 1 else 0)) + + (if (t matches Sum.inl (Sum.inr _)) then + ghostChargeK M ν * ENNReal.ofReal ε else 0) := by + classical + -- Rewrite the telescoped carried-bad mass as the weighted post-step bad mass. + rw [OracleComp.ProgramLogic.Relational.tsum_tsum_postStepOutM_mul + (ghostHybridImpl ids M maxAttempts true pk sk) ν t (fun s => if s.2 = true then 1 else 0)] + rcases t with (n | mc) | msg + · -- Uniform step: flag preserved. + rw [if_neg (by simp), add_zero] + refine ENNReal.tsum_le_tsum fun p => ?_ + gcongr + refine ghostHybridImpl_flag_preserved_le M _ p ?_ + intro z hz + simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨_, -, rfl⟩ := hz; rfl + · -- Read step: pays the per-target membership charge, bounded via the invariant by `K ν · ε`. + rw [if_pos (by simp)] + calc (∑' p : GhostState M Commit Chal, ν p * + ∑' z : Chal × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk (.inl (.inr mc))).run p] * + (if z.2.2 = true then 1 else 0)) + ≤ ∑' p : GhostState M Commit Chal, ν p * + ((if p.2 = true then 1 else 0) + memCharge M p.1.1.2 mc) := + ENNReal.tsum_le_tsum fun p => by + gcongr + exact ghostHybridImpl_read_expected_flag_le ids M maxAttempts pk sk mc p + _ = (∑' p : GhostState M Commit Chal, ν p * (if p.2 = true then 1 else 0)) + + ∑' p : GhostState M Commit Chal, ν p * memCharge M p.1.1.2 mc := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun p => by rw [mul_add] + _ ≤ (∑' p : GhostState M Commit Chal, ν p * (if p.2 = true then 1 else 0)) + + ghostChargeK M ν * ENNReal.ofReal ε := by + gcongr + exact _hInv mc + · -- Sign step: the signing handler leaves `s.2` untouched, so the flag is preserved. + rw [if_neg (by simp), add_zero] + refine ENNReal.tsum_le_tsum fun p => ?_ + gcongr + refine ghostHybridImpl_flag_preserved_le M _ p ?_ + intro z hz + simp only [ghostHybridImpl, StateT.run_mk, support_map] at hz + obtain ⟨_, -, rfl⟩ := hz; rfl + +omit [SampleableType Stmt] in +/-- **`h_K` premise of the threaded bound for the ghost handler.** The ghost-size accumulator +`ghostChargeK` telescopes across one step, growing by `≤ ofReal (1/(1-p)) · mass ν` on a sign +step (`ghostHybridImpl_sign_expected_enncard_le`); reads and uniform steps preserve it (the +ghost layer is untouched). -/ +lemma avgBadM_ghostHybridImpl_threaded_K + (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (_hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (_hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (ν : GhostState M Commit Chal → ℝ≥0∞) + (_hInv : ghostChargeInv M ε ν) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) : + (∑' u : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t, + ghostChargeK M + (OracleComp.ProgramLogic.Relational.postStepOutM + (ghostHybridImpl ids M maxAttempts true pk sk) ν t u)) + ≤ ghostChargeK M ν + + (if (t matches Sum.inr _) then + ENNReal.ofReal (1 / (1 - p_abort)) * + (∑' p : GhostState M Commit Chal, ν p) else 0) := by + classical + -- Rewrite `∑'u K(postStepOutM ν t u)` as the weighted post-step charge + -- for `F := enncard ∘ ghost`. + have hrw : (∑' u : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t, + ghostChargeK M + (OracleComp.ProgramLogic.Relational.postStepOutM + (ghostHybridImpl ids M maxAttempts true pk sk) ν t u)) + = ∑' p : GhostState M Commit Chal, ν p * + ∑' z : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × GhostState M Commit Chal, + Pr[= z | (ghostHybridImpl ids M maxAttempts true pk sk t).run p] * + QueryCache.enncard (z.2.1.1.2) := by + rw [← OracleComp.ProgramLogic.Relational.tsum_tsum_postStepOutM_mul + (ghostHybridImpl ids M maxAttempts true pk sk) ν t (fun s => QueryCache.enncard s.1.1.2)] + rfl + rw [hrw, ghostChargeK] + -- Per-state inner charge bound, then `tsum`-monotone fold. + rcases t with (n | mc) | msg + · -- Uniform step: state untouched, ghost charge preserved. + rw [if_neg (by simp), add_zero] + refine ENNReal.tsum_le_tsum fun p => ?_ + exact le_of_eq (congrArg (ν p * ·) + (ghostHybridImpl_unif_expected_enncard ids M maxAttempts pk sk n p)) + · -- Read step: writes only the base layer, ghost charge preserved. + rw [if_neg (by simp), add_zero] + refine ENNReal.tsum_le_tsum fun p => ?_ + exact le_of_eq (congrArg (ν p * ·) + (ghostHybridImpl_read_expected_enncard ids M maxAttempts pk sk mc p)) + · -- Sign step: ghostSignBody grows the ghost size by `≤ ∑ attempts ≤ 1/(1-p)`. + rw [if_pos (by simp)] + rw [mul_comm (ENNReal.ofReal (1 / (1 - p_abort))) _, ← ENNReal.tsum_mul_right, + ← ENNReal.tsum_add] + refine ENNReal.tsum_le_tsum fun p => ?_ + rw [← mul_add] + gcongr + exact ghostHybridImpl_sign_expected_enncard_le ids M maxAttempts pk sk msg hAbort hp₀ hp p + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HopLemmas.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HopLemmas.lean new file mode 100644 index 000000000..d19e42ad9 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/HopLemmas.lean @@ -0,0 +1,983 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.TapeFactorization + +/-! +# EUF-CMA for Fiat-Shamir with aborts: HopLemmas + +The hybrid hop lemmas, stated per key pair under pointwise hypotheses at +that key; the good-key event and `δ` enter only once, in the final averaging over +key generation. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ## Hop lemmas + +Each hop is stated per key pair, under pointwise hypotheses at that key; the good-key +event and `δ` enter only once, in the final averaging over `hr.gen`. -/ + +omit [SampleableType Stmt] in +/-- G₀ bridge: at every key pair produced by key generation, the real-signing hybrid +experiment reproduces the success probability of the standard unforgeability experiment +`SignatureAlg.unforgeableExp` under `runtime M`. + +Distributional content: the runtime's `withStateOracle randomOracle` semantics of the +experiment (with its `WriterT` signing log) coincides with the single-cache-layer +presentation, with the `WriterT` log projected to the signed-message list. The proof is +a `simulateQ` commutation argument in the style of `roSim.run'_liftM_bind` and the +correctness proof in `FiatShamirWithAbort.correct`. -/ +lemma probOutput_unforgeableExp_eq_hybridExpAtKey_real : + Pr[= true | SignatureAlg.unforgeableExp (runtime M) adv] = + Pr[= true | do + let (pk, sk) ← hr.gen + hybridExpAtKey ids hr M maxAttempts adv (realSignBody ids M maxAttempts pk sk) pk] := by + classical + set base : QueryImpl (unifSpec + (M × Commit →ₒ Chal)) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) := + unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) with hbase + -- `base` matches the runtime's `withStateOracle` interpreter: both lift `unifSpec` by + -- `liftTarget` (`unifFwdImpl` is exactly that) and use the caching `randomOracle`. + have hrt : ∀ {α : Type} (oa : OracleComp (unifSpec + (M × Commit →ₒ Chal)) α), + (runtime M).evalDist oa = 𝒟[(simulateQ base oa).run' ∅] := fun {α} oa => by + rw [hbase] + rfl + unfold SignatureAlg.unforgeableExp + rw [hrt] + rw [show (FiatShamirWithAbort ids hr M maxAttempts).keygen = + (liftM hr.gen : OracleComp (unifSpec + (M × Commit →ₒ Chal)) (Stmt × Wit)) from rfl] + rw [simulateQ_bind, roSim.run'_liftM_bind] + refine probOutput_congr rfl ?_ + refine congrArg _ (bind_congr fun pksk => ?_) + obtain ⟨pk, sk⟩ := pksk + simp only [] + rw [hybridExpAtKey_eq_run_bind] + -- Fuse the inner WriterT-logging `simulateQ` pass with the outer cache simulation + -- `simulateQ base` via `writerTMapBase`, so the whole left-hand experiment becomes a + -- single `simulateQ` over the run-normal-form cache base, still carrying the WriterT log. + rw [simulateQ_bind, StateT.run'_eq, StateT.run_bind, + QueryImpl.simulateQ_writerTMapBase_run] + -- Remaining: reconcile the fused WriterT-log-over-`StateT cache` run with the hybrid's + -- flat `StateT (cache × List M)` run. The bridge follows the Sigma-side recipe in + -- `FiatShamir/Sigma/Stateful/Compatibility.lean`: + -- 1. `base.writerTMapBase implW = (toQueryImpl _).liftTarget _ + (realSignBody …).withLogging` + -- (a per-query handler equality; the signing handler is `simulateQ base (sign …) = + -- realSignBody`); + -- 2. `QueryImpl.map_run_withLogging_inputs_eq_run_appendInputLog` rewrites the WriterT log + -- into a `StateT (List M)` input log carrying `[] ++ log.map fst`; + -- 3. `OracleComp.simulateQ_flattenStateT_run` flattens the nested `StateT (List M) + -- (StateT cache ProbComp)` into the hybrid's flat `StateT (cache × List M) ProbComp`; + -- 4. a state-projection (`map_run_simulateQ_eq_of_query_map_eq`) matches the flattened + -- handler against `hybridBaseImpl + hybridSignImpl realSignBody` (the lists differ only + -- by append-vs-prepend ordering, which is invisible to the freshness check); + -- 5. the verify tail matches `hybridVerifyCont` with `wasQueried msg ↔ msg ∈ signed` + -- via `QueryLog.wasQueried_eq_decide_mem_map_fst`. + have hHandler : base.writerTMapBase + ((HasQuery.toQueryImpl (spec := unifSpec + (M × Commit →ₒ Chal)) + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal)))).liftTarget + (WriterT (QueryLog (M →ₒ Option (Commit × Resp))) + (OracleComp (unifSpec + (M × Commit →ₒ Chal)))) + + (FiatShamirWithAbort (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).signingOracle pk sk) = + base.liftTarget + (WriterT (QueryLog (M →ₒ Option (Commit × Resp))) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) + + QueryImpl.withLogging + (fun msg => realSignBody ids M maxAttempts pk sk msg : + QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) := by + funext t + rcases t with bq | sq + · ext s + simp [QueryImpl.writerTMapBase, QueryImpl.add_apply_inl, QueryImpl.liftTarget_apply, + HasQuery.toQueryImpl_apply, base, unifFwdImpl] + · ext s + simp [QueryImpl.writerTMapBase, QueryImpl.add_apply_inr, SignatureAlg.signingOracle, + QueryImpl.withLogging_apply, FiatShamirWithAbort, realSignBody, base] + rw [hHandler] + -- Provide the cache base as a `HasQuery` instance so the WriterT-log → input-list replay + -- lemma `QueryImpl.map_run_withLogging_inputs_eq_run_appendInputLog` matches + -- `base.liftTarget _` (it equals `(HasQuery.toQueryImpl).liftTarget _` for this instance). + letI hq : HasQuery (unifSpec + (M × Commit →ₒ Chal)) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) := base.toHasQuery + -- Replay the WriterT log into a `StateT (List M)` input log, flatten the nested + -- `StateT (List M) (StateT cache ProbComp)` to `StateT (List M × cache) ProbComp`, and + -- match the flattened handler against `hybridBaseImpl + hybridSignImpl realSignBody` under + -- the state swap `(List M × cache) → (cache × List M)`. + set so : QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) := + (fun msg => realSignBody ids M maxAttempts pk sk msg) with hso + -- (a) the WriterT-log run, mapped to `(out, log.map fst)`, equals the `appendInputLog` run. + have hreplay := QueryImpl.map_run_withLogging_inputs_eq_run_appendInputLog + (spec₀ := unifSpec + (M × Commit →ₒ Chal)) (loggedSpec := M →ₒ Option (Commit × Resp)) + (m₀ := StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) + so (adv.main pk) ([] : List M) + simp only [] at hreplay + -- The flattened `appendInputLog` handler. + set implAppend : QueryImpl + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (List M) (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) := + (HasQuery.toQueryImpl (spec := unifSpec + (M × Commit →ₒ Chal)) + (m := StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)).liftTarget + (StateT (List M) (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) + + QueryImpl.appendInputLog so with himplAppend + -- (c) the flattened handler equals `hybridBaseImpl + hybridSignImpl realSignBody` after + -- swapping the joint state `(List M × cache) → (cache × List M)`. + -- `proj` swaps the components and reverses the list: the hybrid prepends each signed + -- message (`msg :: l`) while `appendInputLog` appends it (`l ++ [msg]`), and reversing + -- reconciles the two orderings step by step. + set proj : List M × (M × Commit →ₒ Chal).QueryCache → + (M × Commit →ₒ Chal).QueryCache × List M := fun s => (s.2, s.1.reverse) with hproj + have hmatch : ∀ (t : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Domain) + (s : List M × (M × Commit →ₒ Chal).QueryCache), + Prod.map id proj <$> (implAppend.flattenStateT t).run s = + ((hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M so) t).run (proj s) := by + rintro ((tu | tro) | tsign) ⟨l, c⟩ + · simp only [hproj, himplAppend, QueryImpl.flattenStateT, QueryImpl.add_apply_inl, + QueryImpl.liftTarget_apply, HasQuery.toQueryImpl_apply, hybridBaseImpl, unifFwdImpl] + rfl + · have hlhs : (implAppend.flattenStateT (Sum.inl (Sum.inr tro))).run (l, c) = + roStep M c tro >>= fun a => pure (a.1, (l, a.2)) := by + rw [himplAppend] + simp only [QueryImpl.flattenStateT, QueryImpl.add_apply_inl, QueryImpl.liftTarget_apply, + StateT.run_mk] + erw [StateT.run_monadLift] + have hbq : (HasQuery.toQueryImpl (spec := unifSpec + (M × Commit →ₒ Chal)) + (m := StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) (Sum.inr tro)).run c + = roStep M c tro := randomOracle_run_eq_roStep M c tro + rw [StateT.run_bind] + erw [hbq] + simp [map_eq_bind_pure_comp, bind_assoc, pure_bind, Function.comp, monad_norm] + rw [hlhs, hproj] + simp only [QueryImpl.add_apply_inl] + erw [hybridBaseImpl_run_ro] + simp [map_eq_bind_pure_comp, bind_assoc, Function.comp] + · have hlhs : (implAppend.flattenStateT (Sum.inr tsign)).run (l, c) = + (so tsign).run c >>= fun a => pure (a.1, (l ++ [tsign], a.2)) := by + simp [himplAppend, QueryImpl.flattenStateT, QueryImpl.add_apply_inr, + QueryImpl.appendInputLog_apply, StateT.run_mk, StateT.run_bind, StateT.run_monadLift, + StateT.run_modifyGet, modify, map_eq_bind_pure_comp, bind_assoc, Function.comp, + monad_norm] + rw [hlhs, hproj] + simp only [QueryImpl.add_apply_inr] + erw [hybridSignImpl_run] + simp [map_eq_bind_pure_comp, bind_assoc, Function.comp, List.reverse_append] + have hflat := fun {β : Type} + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) β) (s : List M × (M × Commit →ₒ Chal).QueryCache) => + OracleComp.map_run_simulateQ_eq_of_query_map_eq implAppend.flattenStateT + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + hybridSignImpl M so) + proj hmatch oa s + -- Final assembly (steps b/d): chain `hreplay` (WriterT-log → `appendInputLog`), + -- `OracleComp.simulateQ_flattenStateT_run` (flatten the nested `StateT (List M) (StateT cache)` + -- to `StateT (List M × cache)`), and `hflat` (the `proj`-projection to the hybrid run on + -- `(cache × List M)`), then identify the verify tail with `hybridVerifyCont` using + -- `QueryLog.wasQueried_eq_decide_mem_map_fst` (`wasQueried msg ↔ msg ∈ log.map fst ↔ + -- msg ∈ (final signed list).reverse`, membership-invariant under the `proj` list reversal). + -- (b) Apply `.run ∅` to `hreplay` (a `StateT cache` identity) to obtain a `ProbComp` + -- identity for the cache-run of the WriterT log, with the log already projected to its + -- list of queried messages. + have hreplay' := congrArg + (fun (g : StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp _) => g.run ∅) hreplay + simp only [StateT.run_map] at hreplay' + -- (c) Flatten the nested `StateT (List M) (StateT cache)` run into the joint-state run. + have hflatten := OracleComp.simulateQ_flattenStateT_run implAppend (adv.main pk) ([] : List M) + (∅ : (M × Commit →ₒ Chal).QueryCache) + -- (d) Project the joint-state run onto the hybrid run via `proj`. + have hflatHybrid := hflat (adv.main pk) (([], ∅) : List M × (M × Commit →ₒ Chal).QueryCache) + rw [hproj] at hflatHybrid + simp only [List.reverse_nil] at hflatHybrid + -- Rewrite the hybrid run on the right as a pure relabelling of the cache-run of the + -- WriterT-logged adversary, sending `(((msg, σ), log), cache)` to + -- `((msg, σ), (cache, (log.map fst).reverse))`. + rw [← hflatHybrid, hflatten, ← hreplay'] + simp only [map_bind, bind_assoc, map_pure, pure_bind, Prod.map, id] + -- The cache base appearing in the left generator is exactly the `HasQuery.toQueryImpl` + -- instance used by the replayed run (`hq := base.toHasQuery`). + rw [show (HasQuery.toQueryImpl (spec := unifSpec + (M × Commit →ₒ Chal)) + (m := StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) = base from rfl] + -- Push the relabelling map into the bind so both sides bind over the same generator. + rw [bind_map_left] + refine bind_congr fun p => ?_ + -- For each WriterT-run outcome `p = (((msg, σ), log), cache)`, the left verify tail equals + -- `hybridVerifyCont` at the relabelled state `((msg, σ), (cache, (log.map fst).reverse))`. + obtain ⟨⟨⟨msg, σ⟩, log⟩, cache⟩ := p + simp only [hybridVerifyCont] + rw [simulateQ_bind] + simp only [simulateQ_pure, StateT.run_bind, StateT.run', map_bind, bind_map_left] + refine bind_congr fun verified => ?_ + obtain ⟨ok, c⟩ := verified + simp only [StateT.run_pure, map_pure, List.nil_append, List.mem_reverse, + QueryLog.wasQueried_eq_decide_mem_map_fst, decide_not] + -- Both sides are `!decide (msg ∈ log.map fst) && ok`; they differ only in the choice of + -- `Decidable` instance for the membership test, which is a subsingleton, so `decide` + -- agrees on the nose after normalising. + norm_num [Bool.and_left_comm] + +/-- Lift a cache-level hybrid handler to one carrying a never-touched bad flag in its +state, so the `expectedQuerySlack` bridge of `ProgramLogic/Relational/SimulateQ.lean` +applies. The flag is preserved on every step, hence stays `false` along any run started +from `false`. -/ +noncomputable def flagLift {ι : Type} {spec : OracleSpec ι} {σ : Type} + (impl : QueryImpl spec (StateT σ ProbComp)) : + QueryImpl spec (StateT (σ × Bool) ProbComp) := + fun t => StateT.mk fun p => + (fun us : spec.Range t × σ => (us.1, (us.2, p.2))) <$> (impl t).run p.1 + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +lemma flagLift_run {ι : Type} {spec : OracleSpec ι} {σ : Type} + (impl : QueryImpl spec (StateT σ ProbComp)) (t : spec.Domain) (s : σ) (b : Bool) : + ((flagLift impl t).run (s, b)) = + (fun us : spec.Range t × σ => (us.1, (us.2, b))) <$> (impl t).run s := rfl + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +/-- Transport a predicate-targeted query bound across a (propositionally equal) choice of +predicate and `DecidablePred` instance. The predicate is allowed to differ by its match +auxiliary (which arises when the same matches-notation is elaborated in different +modules), and the decidability instance is a subsingleton. -/ +lemma isQueryBoundP_cast_pred {ι : Type} {spec : OracleSpec ι} {α : Type} + {oa : OracleComp spec α} {p₁ p₂ : spec.Domain → Prop} + {i₁ : DecidablePred p₁} {i₂ : DecidablePred p₂} {n : ℕ} (hp : p₁ = p₂) + (h : @OracleComp.IsQueryBoundP _ spec α oa p₁ i₁ n) : + @OracleComp.IsQueryBoundP _ spec α oa p₂ i₂ n := by + subst hp + convert h using 2 + +/-- Arithmetic kernel of the Sign → Prog charge: the discrete first moment of a truncated +geometric series is dominated by the square of its zeroth moment, `∑_{a Finset.range i) with hQ + have hLHS : ∑ a ∈ Finset.range m, (a : ℝ) * p ^ a = ∑ q ∈ Q, p ^ q.1 := by + rw [hQ, Finset.sum_sigma] + refine Finset.sum_congr rfl fun i hi => ?_ + simp only + rw [Finset.sum_const, Finset.card_range, nsmul_eq_mul] + rw [hLHS, show (∑ a ∈ P, p ^ a.1 * p ^ a.2) = ∑ a ∈ P, p ^ (a.1 + a.2) from + Finset.sum_congr rfl fun a _ => by rw [pow_add]] + have himg : ∑ q ∈ Q, p ^ q.1 + = ∑ r ∈ Q.image (fun q => (q.1 - (q.2 + 1), q.2 + 1)), p ^ (r.1 + r.2) := by + rw [Finset.sum_image] + · refine Finset.sum_congr rfl fun q hq => ?_ + rw [hQ, Finset.mem_sigma, Finset.mem_range, Finset.mem_range] at hq + congr 1 + omega + · intro a ha b hb hab + rw [Finset.mem_coe, hQ, Finset.mem_sigma, Finset.mem_range, Finset.mem_range] at ha hb + simp only [Prod.mk.injEq] at hab + obtain ⟨h1, h2⟩ := hab + obtain ⟨a1, a2⟩ := a + obtain ⟨b1, b2⟩ := b + simp only at * + have hsnd : a2 = b2 := by omega + subst hsnd + have hfst : a1 = b1 := by omega + subst hfst + rfl + rw [himg] + refine Finset.sum_le_sum_of_subset_of_nonneg ?_ (fun r _ _ => by positivity) + intro r hr + rw [Finset.mem_image] at hr + obtain ⟨q, hq, rfl⟩ := hr + rw [hQ, Finset.mem_sigma, Finset.mem_range, Finset.mem_range] at hq + rw [hP, Finset.mem_product, Finset.mem_range, Finset.mem_range] + omega + +omit [SampleableType Stmt] in +/-- Hop G₀ → G₁ (Sign → Prog) at a fixed key: replacing the caching hash of each signing +attempt by overwrite-reprogramming with a fresh challenge costs at most + +`qS·ε·((qS+1)/(2·(1-p)²) + (qH+1)/(1-p))`. + +Distributional content (identical-until-bad): the two games agree unless some signing +attempt commits to a point `(msg, w)` already present in the cache. Conditioned on good +keys, each attempt's commitment is `ε`-guessable (`hGuess`), the cache holds at most +`qH + 1` adversarial entries plus the entries of previous signing attempts, and the +expected number of attempts per signing query is at most `1/(1-p)` (`hAbort`, via +`sign_expectedQueries_le_geometric`). Intended vehicle: +`tvDist_simulateQ_le_probEvent_bad` (the fundamental lemma in +`ProgramLogic/Relational/SimulateQ.lean`) with the bad event tracked on the hybrid +state, plus the expected-attempt-count machinery of `WithAbort/ExpectedCost.lean`. + +The abort probability is assumed to lie in `[0, 1)` (`hp₀`, `hp`). Nonnegativity is what +keeps the geometric factors `1/(1 - p_abort)` at least `1`, so that the claimed loss +dominates the adversarial-collision gap `qS·qH·ε` already present in an abort-free scheme; +the bound `0 ≤ p_abort` is available at every call site from the good-key event. -/ +lemma probOutput_hybridExpAtKey_real_le_prog + (qS qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk] ≤ + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] + + ENNReal.ofReal (qS * ε * (qS + 1) / (2 * (1 - p_abort) ^ 2) + + qS * (qH + 1) * ε / (1 - p_abort)) := by + classical + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + set σ := (M × Commit →ₒ Chal).QueryCache × List M with hσ + -- The combined cache-level handlers for the two games. + set implReal : QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT σ ProbComp) := + hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (realSignBody ids M maxAttempts pk sk) with himplReal + set implProg : QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT σ ProbComp) := + hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (progSignBody ids M pk sk · maxAttempts) with himplProg + set R : σ → ℝ≥0∞ := fun s => QueryCache.enncard s.1 with hR + set ζ : ℝ≥0∞ := ENNReal.ofReal ε * + ∑ a ∈ Finset.range maxAttempts, (a : ℝ≥0∞) * ENNReal.ofReal p_abort ^ a with hζ + set β : ℝ≥0∞ := ENNReal.ofReal ε * + ∑ a ∈ Finset.range maxAttempts, ENNReal.ofReal p_abort ^ a with hβ + set g : ℝ≥0∞ := ∑ a ∈ Finset.range maxAttempts, ENNReal.ofReal p_abort ^ a with hg + set querySlack : σ → ℝ≥0∞ := fun s => ζ + R s * β with hquerySlack + -- The per-charged-query TV slack: real-vs-prog within a single signing query. + have h_step_tv_charged : ∀ (t : _), (· matches .inr _) t → ∀ (s : σ), + ENNReal.ofReal (tvDist ((flagLift implProg t).run (s, false)) + ((flagLift implReal t).run (s, false))) ≤ querySlack s := by + rintro (t' | msg) hc s + · exact absurd hc (by simp) + rcases s with ⟨c, l⟩ + -- Both flag-lifted signing runs are a single (shared, injective) map over the + -- corresponding cache-level signing body; the map drops out of the TV distance, + -- and the body-level TV is the proven `signCollisionBound`. + have hrunProg : (flagLift implProg (Sum.inr msg)).run ((c, l), false) = + (fun x : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (x.1, ((x.2, msg :: l), false))) <$> + (progSignBody ids M pk sk msg maxAttempts).run c := by + rw [flagLift_run, himplProg, QueryImpl.add_apply_inr] + change (fun us => (us.1, us.2, false)) <$> + ((fun ac : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (ac.1, (ac.2, msg :: l))) <$> (progSignBody ids M pk sk msg maxAttempts).run c) = _ + rw [Functor.map_map] + have hrunReal : (flagLift implReal (Sum.inr msg)).run ((c, l), false) = + (fun x : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (x.1, ((x.2, msg :: l), false))) <$> + (realSignBody ids M maxAttempts pk sk msg).run c := by + rw [flagLift_run, himplReal, QueryImpl.add_apply_inr] + change (fun us => (us.1, us.2, false)) <$> + ((fun ac : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (ac.1, (ac.2, msg :: l))) <$> (realSignBody ids M maxAttempts pk sk msg).run c) = _ + rw [Functor.map_map] + rw [hrunProg, hrunReal] + refine le_trans (ENNReal.ofReal_le_ofReal + (le_trans (tvDist_map_le _ _ _) (le_of_eq (tvDist_comm _ _)))) ?_ + refine le_trans (ofReal_tvDist_run_fsAbortSignLoop_progSignBody_le + ids M pk sk msg hGuess hAbort maxAttempts c) ?_ + rw [signCollisionBound_eq, hquerySlack, hζ, hβ, hR] + -- Uncharged (base) queries: the two handlers coincide. + have h_step_eq_uncharged : ∀ (t : _), ¬ (· matches .inr _) t → ∀ (p : σ × Bool), + (flagLift implProg t).run p = (flagLift implReal t).run p := by + rintro (t' | msg) hnc p + · rw [flagLift_run, flagLift_run, himplProg, himplReal, + QueryImpl.add_apply_inl, QueryImpl.add_apply_inl] + · exact absurd rfl hnc + -- The flag is never set: monotonicity is vacuous-by-preservation. + have h_mono₁ : ∀ (t : _) (p : σ × Bool), p.2 = true → + ∀ z ∈ support ((flagLift implProg t).run p), z.2.2 = true := by + intro t p hp2 z hz + rw [flagLift_run, support_map] at hz + obtain ⟨us, -, rfl⟩ := hz + exact hp2 + -- Expected-resource hypotheses for `expectedQuerySlack_expected_resource_le`. + have h_charged : ∀ (t : _) (p : σ × Bool), p.2 = false → (· matches .inr _) t → + ∑' z : _ × σ × Bool, Pr[= z | (flagLift implProg t).run p] * R z.2.1 ≤ R p.1 + g := by + rintro (t' | msg) p - hc + · exact absurd hc (by simp) + rcases p with ⟨⟨c, l⟩, b⟩ + -- Reduce the flag-lifted signing run to the `progSignBody` cache-growth tsum. + -- The combined-spec `Range (Sum.inr msg)` index of the tsum is only defeq (not + -- syntactically equal) to `Option (Commit × Resp)`, so we `change` into the + -- explicit type and rewrite the run as a single map over `progSignBody`. + have hrun : (flagLift implProg (Sum.inr msg)).run ((c, l), b) = + (fun x : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (x.1, ((x.2, msg :: l), b))) <$> + (progSignBody ids M pk sk msg maxAttempts).run c := by + rw [flagLift_run, himplProg, QueryImpl.add_apply_inr] + change (fun us => (us.1, us.2, b)) <$> + ((fun ac : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (ac.1, (ac.2, msg :: l))) <$> (progSignBody ids M pk sk msg maxAttempts).run c) = _ + rw [Functor.map_map] + rw [hrun] + change (∑' z : Option (Commit × Resp) × σ × Bool, + Pr[= z | (fun x : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache => + (x.1, ((x.2, msg :: l), b))) <$> + (progSignBody ids M pk sk msg maxAttempts).run c] * R z.2.1) ≤ _ + rw [map_eq_bind_pure_comp, tsum_probOutput_bind_mul] + simp only [Function.comp, tsum_probOutput_pure_mul] + exact tsum_probOutput_run_progSignBody_mul_enncard_le ids M pk sk msg hAbort maxAttempts c + have h_growth : ∀ (t : _) (p : σ × Bool), p.2 = false → + ¬ (· matches .inr _) t → (· matches .inl (.inr _)) t → + ∀ z ∈ support ((flagLift implProg t).run p), R z.2.1 ≤ R p.1 + 1 := by + rintro ((n | mc) | msg) p - hnc hg z hz + · exact absurd hg (by simp) + · rcases p with ⟨⟨c, l⟩, b⟩ + rw [flagLift_run, himplProg, QueryImpl.add_apply_inl] at hz + replace hz : z ∈ support ((fun us : Chal × σ => (us.1, (us.2, b))) <$> + ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => (cu.1, (cu.2, l))) <$> + roStep M c mc)) := by + rw [← hybridBaseImpl_run_ro]; exact hz + simp only [support_map] at hz + obtain ⟨cu', ⟨cu'', hcu'', rfl⟩, rfl⟩ := hz + -- The random-oracle step grows the cache by at most one entry. + simp only [hR] + rcases hmc : c mc with _ | v + · rw [roStep_of_none M hmc] at hcu'' + simp only [support_bind, support_pure, Set.mem_iUnion, Set.mem_singleton_iff] at hcu'' + obtain ⟨ch, -, rfl⟩ := hcu'' + exact QueryCache.enncard_cacheQuery_le c mc ch + · rw [roStep_of_some M hmc] at hcu'' + rw [(by simpa using hcu'' : cu'' = (v, c))] + exact le_self_add + · exact absurd hg (by simp) + have h_free : ∀ (t : _) (p : σ × Bool), p.2 = false → + ¬ (· matches .inr _) t → ¬ (· matches .inl (.inr _)) t → + ∀ z ∈ support ((flagLift implProg t).run p), R z.2.1 ≤ R p.1 := by + rintro ((n | mc) | msg) p - hnc hng z hz + · -- Uniform query: forwarded without touching the cache. + rcases p with ⟨⟨c, l⟩, b⟩ + have hrun : (hybridBaseImpl (Commit := Commit) (Chal := Chal) M (.inl n)).run + (c, l) = (fun x => (x, (c, l))) <$> + (liftM (unifSpec.query n) : ProbComp (unifSpec.Range n)) := by + simp only [hybridBaseImpl, QueryImpl.add_apply_inl] + rfl + rw [flagLift_run, himplProg, QueryImpl.add_apply_inl] at hz + replace hz : z ∈ support ((fun us : unifSpec.Range n × σ => (us.1, (us.2, b))) <$> + ((fun x : unifSpec.Range n => (x, ((c, l) : σ))) <$> + (liftM (unifSpec.query n) : ProbComp (unifSpec.Range n)))) := by + rw [← hrun]; exact hz + simp only [support_map] at hz + obtain ⟨x, ⟨y, -, rfl⟩, rfl⟩ := hz + exact le_rfl + · exact absurd rfl hng + · exact absurd rfl hnc + -- The bridge: run-level TV ≤ accumulated slack + Pr[bad]. + open OracleComp.ProgramLogic.Relational in + have h_bridge : + ENNReal.ofReal (tvDist + ((simulateQ (flagLift implProg) (adv.main pk)).run ((∅, []), false)) + ((simulateQ (flagLift implReal) (adv.main pk)).run ((∅, []), false))) + ≤ expectedQuerySlack (flagLift implProg) + (· matches .inr _) querySlack (adv.main pk) qS (((∅, []) : σ), false) + + Pr[fun z : _ × σ × Bool => z.2.2 = true | + (simulateQ (flagLift implProg) (adv.main pk)).run (((∅, []) : σ), false)] := by + refine ofReal_tvDist_simulateQ_run_le_expectedQuerySlack_plus_probEvent_output_bad + (flagLift implProg) (flagLift implReal) (· matches .inr _) querySlack + h_step_tv_charged h_step_eq_uncharged h_mono₁ (adv.main pk) + (queryBudget := qS) ?_ (((∅, []) : σ), false) + exact isQueryBoundP_cast_pred (by funext x; rcases x with (_ | _) | _ <;> rfl) (hQ pk).1 + -- The bad-flag probability vanishes: the flag is preserved from `false`. + have h_bad_zero : Pr[fun z : _ × σ × Bool => z.2.2 = true | + (simulateQ (flagLift implProg) (adv.main pk)).run (((∅, []) : σ), false)] = 0 := by + refine probEvent_eq_zero fun z hz hbad => ?_ + have hinv : ∀ y ∈ support ((simulateQ (flagLift implProg) (adv.main pk)).run + (((∅, []) : σ), false)), y.2.2 = false := by + refine OracleComp.simulateQ_run_preserves_inv_of_query (flagLift implProg) + (fun s : σ × Bool => s.2 = false) (fun t s hs y hy => ?_) (adv.main pk) + (((∅, []) : σ), false) rfl + rw [flagLift_run, support_map] at hy + obtain ⟨us, -, rfl⟩ := hy + exact hs + rw [hinv z hz] at hbad + exact absurd hbad (by decide) + -- The accumulated slack is bounded by the resource estimate. + have h_slack_le : OracleComp.ProgramLogic.Relational.expectedQuerySlack (flagLift implProg) + (· matches .inr _) querySlack (adv.main pk) qS (((∅, []) : σ), false) + ≤ (qS : ℝ≥0∞) * ζ + + ((qS : ℝ≥0∞) * R ((∅, []) : σ) + (qS : ℝ≥0∞) * (qH : ℝ≥0∞) + + (qS.choose 2 : ℝ≥0∞) * g) * β := by + refine OracleComp.ProgramLogic.Relational.expectedQuerySlack_expected_resource_le + (flagLift implProg) (· matches .inr _) (· matches .inl (.inr _)) R ζ β g + h_charged h_growth h_free (adv.main pk) (qS := qS) (qH := qH) ?_ ?_ ((∅, []) : σ) + · exact isQueryBoundP_cast_pred (by funext x; rcases x with (_ | _) | _ <;> rfl) (hQ pk).1 + · exact isQueryBoundP_cast_pred (by funext x; rcases x with (_ | _) | _ <;> rfl) (hQ pk).2 + -- The flag-lifted run TV is bounded by the accumulated slack (the bad term vanishes). + set slack : ℝ≥0∞ := (qS : ℝ≥0∞) * ζ + + ((qS : ℝ≥0∞) * R ((∅, []) : σ) + (qS : ℝ≥0∞) * (qH : ℝ≥0∞) + + (qS.choose 2 : ℝ≥0∞) * g) * β with hslack + have h_flag_tv : ENNReal.ofReal (tvDist + ((simulateQ (flagLift implProg) (adv.main pk)).run ((∅, []), false)) + ((simulateQ (flagLift implReal) (adv.main pk)).run ((∅, []), false))) ≤ slack := by + refine le_trans h_bridge ?_ + rw [h_bad_zero, add_zero] + exact h_slack_le + -- Project the flag away: the flag-lifted runs map onto the (unflagged) hybrid runs. + have hprojP : ∀ (t : _) (sb : σ × Bool), + Prod.map id (Prod.fst : σ × Bool → σ) <$> (flagLift implProg t).run sb = + (implProg t).run sb.1 := by + intro t sb + rw [flagLift_run, Functor.map_map] + simp only [Prod.map, id_eq, Prod.mk.eta, id_map'] + have hprojR : ∀ (t : _) (sb : σ × Bool), + Prod.map id (Prod.fst : σ × Bool → σ) <$> (flagLift implReal t).run sb = + (implReal t).run sb.1 := by + intro t sb + rw [flagLift_run, Functor.map_map] + simp only [Prod.map, id_eq, Prod.mk.eta, id_map'] + have hrunProj_P : (simulateQ implProg (adv.main pk)).run (∅, []) = + Prod.map id (Prod.fst : σ × Bool → σ) <$> + (simulateQ (flagLift implProg) (adv.main pk)).run ((∅, []), false) := + (OracleComp.map_run_simulateQ_eq_of_query_map_eq (flagLift implProg) implProg + (Prod.fst : σ × Bool → σ) hprojP (adv.main pk) ((∅, []), false)).symm + have hrunProj_R : (simulateQ implReal (adv.main pk)).run (∅, []) = + Prod.map id (Prod.fst : σ × Bool → σ) <$> + (simulateQ (flagLift implReal) (adv.main pk)).run ((∅, []), false) := + (OracleComp.map_run_simulateQ_eq_of_query_map_eq (flagLift implReal) implReal + (Prod.fst : σ × Bool → σ) hprojR (adv.main pk) ((∅, []), false)).symm + -- Hence the unflagged run TV is also bounded by the slack. + have h_run_tv : ENNReal.ofReal (tvDist + ((simulateQ implProg (adv.main pk)).run (∅, [])) + ((simulateQ implReal (adv.main pk)).run (∅, []))) ≤ slack := by + rw [hrunProj_P, hrunProj_R] + exact le_trans (ENNReal.ofReal_le_ofReal (tvDist_map_le _ _ _)) h_flag_tv + -- Lift the run-level bound to the games through the shared verification continuation. + have h_games_tv : ENNReal.ofReal (tvDist + (hybridExpAtKey ids hr M maxAttempts adv (realSignBody ids M maxAttempts pk sk) pk) + (hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk)) ≤ slack := by + rw [hybridExpAtKey_eq_run_bind, hybridExpAtKey_eq_run_bind, tvDist_comm] + refine le_trans (ENNReal.ofReal_le_ofReal (tvDist_bind_right_le _ _ _)) ?_ + rw [← himplProg, ← himplReal] + exact h_run_tv + -- Convert the game-level TV bound into the probability-output inequality. + have h_prob : Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk] ≤ + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] + slack := by + have habs := abs_probOutput_toReal_sub_le_tvDist + (hybridExpAtKey ids hr M maxAttempts adv (realSignBody ids M maxAttempts pk sk) pk) + (hybridExpAtKey ids hr M maxAttempts adv (progSignBody ids M pk sk · maxAttempts) pk) + have h2 := (abs_le.mp habs).2 + calc Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk] + = ENNReal.ofReal (Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk].toReal) := + (ENNReal.ofReal_toReal probOutput_ne_top).symm + _ ≤ ENNReal.ofReal (Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk].toReal + + tvDist (hybridExpAtKey ids hr M maxAttempts adv + (realSignBody ids M maxAttempts pk sk) pk) + (hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk)) := by + refine ENNReal.ofReal_le_ofReal ?_; linarith [h2] + _ = Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] + + ENNReal.ofReal (tvDist _ _) := by + rw [ENNReal.ofReal_add ENNReal.toReal_nonneg (tvDist_nonneg _ _), + ENNReal.ofReal_toReal probOutput_ne_top] + _ ≤ Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] + slack := + add_le_add le_rfl h_games_tv + -- Close: `slack ≤ ofReal(target)` via the `ℝ≥0∞` arithmetic. + refine le_trans h_prob (add_le_add le_rfl ?_) + rw [hslack] + -- The starting cache is empty, so the resource base `R ∅` vanishes. + have hR0 : R ((∅, []) : σ) = 0 := by rw [hR]; exact QueryCache.enncard_empty + rw [hR0] + rcases lt_or_ge ε 0 with hε | hε + · -- `ε < 0`: the `ofReal ε` factors collapse `ζ` and `β` to `0`. + have h0 : ENNReal.ofReal ε = 0 := ENNReal.ofReal_eq_zero.mpr hε.le + have hζ0 : ζ = 0 := by rw [hζ, h0, zero_mul] + have hβ0 : β = 0 := by rw [hβ, h0, zero_mul] + rw [hζ0, hβ0, mul_zero, mul_zero, zero_add] + exact bot_le + · -- Main case: convert the `ℝ≥0∞` slack into `ofReal` of a real expression. + set S : ℝ := ∑ a ∈ Finset.range maxAttempts, p_abort ^ a with hSdef + set Tm : ℝ := ∑ a ∈ Finset.range maxAttempts, (a : ℝ) * p_abort ^ a with hTdef + have hSnn : 0 ≤ S := Finset.sum_nonneg fun a _ => pow_nonneg hp₀ a + have hTnn : 0 ≤ Tm := + Finset.sum_nonneg fun a _ => mul_nonneg (Nat.cast_nonneg a) (pow_nonneg hp₀ a) + have hg_eq : g = ENNReal.ofReal S := by + rw [hg, hSdef, ENNReal.ofReal_sum_of_nonneg (fun a _ => pow_nonneg hp₀ a)] + exact Finset.sum_congr rfl fun a _ => by rw [← ENNReal.ofReal_pow hp₀] + have hTsum : (∑ a ∈ Finset.range maxAttempts, (a : ℝ≥0∞) * ENNReal.ofReal p_abort ^ a) + = ENNReal.ofReal Tm := by + rw [hTdef, ENNReal.ofReal_sum_of_nonneg + (fun a _ => mul_nonneg (Nat.cast_nonneg a) (pow_nonneg hp₀ a))] + exact Finset.sum_congr rfl fun a _ => by + rw [ENNReal.ofReal_mul (Nat.cast_nonneg a), ← ENNReal.ofReal_pow hp₀, + ENNReal.ofReal_natCast] + have hζ_eq : ζ = ENNReal.ofReal (ε * Tm) := by + rw [hζ, hTsum, ← ENNReal.ofReal_mul hε] + have hβ_eq : β = ENNReal.ofReal (ε * S) := by + rw [hβ, hg_eq, ← ENNReal.ofReal_mul hε] + -- The convolution bound `∑ a·pᵃ ≤ (∑ pᵃ)²` and the geometric bound `∑ pᵃ ≤ 1/(1-p)`. + have hTS : Tm ≤ S ^ 2 := by + rw [hTdef, hSdef]; exact sum_natCast_mul_pow_le_sq_sum_pow p_abort hp₀ maxAttempts + have hSgeo : S ≤ 1 / (1 - p_abort) := by + rw [hSdef, le_div_iff₀ h1p] + have hmul := geom_sum_mul p_abort maxAttempts + nlinarith [pow_nonneg hp₀ maxAttempts] + rw [hζ_eq, hβ_eq, hg_eq, mul_zero, zero_add, + show (qS : ℝ≥0∞) = ENNReal.ofReal qS from (ENNReal.ofReal_natCast qS).symm, + show (qH : ℝ≥0∞) = ENNReal.ofReal qH from (ENNReal.ofReal_natCast qH).symm, + show (qS.choose 2 : ℝ≥0∞) = ENNReal.ofReal (qS.choose 2) from + (ENNReal.ofReal_natCast _).symm] + rw [← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_add (by positivity) (by positivity), + ← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_add (by positivity) (by positivity)] + refine ENNReal.ofReal_le_ofReal ?_ + -- Pure real inequality. + have hchoose : (qS.choose 2 : ℝ) = qS * (qS - 1) / 2 := Nat.cast_choose_two (K := ℝ) qS + have hqS : (0 : ℝ) ≤ qS := Nat.cast_nonneg qS + have hqH : (0 : ℝ) ≤ qH := Nat.cast_nonneg qH + have hS2 : S ^ 2 ≤ 1 / (1 - p_abort) ^ 2 := by + have hsq : S ^ 2 ≤ (1 / (1 - p_abort)) ^ 2 := by gcongr + rwa [div_pow, one_pow] at hsq + have hTle : Tm ≤ 1 / (1 - p_abort) ^ 2 := le_trans hTS hS2 + have ht1 : ↑qS * (ε * Tm) ≤ qS * ε / (1 - p_abort) ^ 2 := by + rw [show (qS : ℝ) * (ε * Tm) = (qS * ε) * Tm by ring, + show (qS : ℝ) * ε / (1 - p_abort) ^ 2 = (qS * ε) * (1 / (1 - p_abort) ^ 2) by ring] + exact mul_le_mul_of_nonneg_left hTle (by positivity) + have ht2 : ↑qS * ↑qH * (ε * S) ≤ qS * qH * ε / (1 - p_abort) := by + rw [show (qS : ℝ) * qH * (ε * S) = (qS * qH * ε) * S by ring, + show (qS : ℝ) * qH * ε / (1 - p_abort) = (qS * qH * ε) * (1 / (1 - p_abort)) by ring] + exact mul_le_mul_of_nonneg_left hSgeo (by positivity) + have ht3 : (qS.choose 2 : ℝ) * (ε * S ^ 2) ≤ (qS.choose 2 : ℝ) * ε / (1 - p_abort) ^ 2 := by + rw [show (qS.choose 2 : ℝ) * (ε * S ^ 2) = ((qS.choose 2 : ℝ) * ε) * S ^ 2 by ring, + show (qS.choose 2 : ℝ) * ε / (1 - p_abort) ^ 2 + = ((qS.choose 2 : ℝ) * ε) * (1 / (1 - p_abort) ^ 2) by ring] + exact mul_le_mul_of_nonneg_left hS2 (by positivity) + have hcomb : ↑qS * (ε * Tm) + (↑qS * ↑qH + ↑(qS.choose 2) * S) * (ε * S) + ≤ qS * ε / (1 - p_abort) ^ 2 + qS * qH * ε / (1 - p_abort) + + (qS.choose 2 : ℝ) * ε / (1 - p_abort) ^ 2 := by + rw [show (↑qS * ↑qH + ↑(qS.choose 2) * S) * (ε * S) + = ↑qS * ↑qH * (ε * S) + (qS.choose 2 : ℝ) * (ε * S ^ 2) by ring] + linarith [ht1, ht2, ht3] + refine le_trans hcomb ?_ + rw [hchoose] + have hne : (1 - p_abort) ^ 2 ≠ 0 := by positivity + have hkey : (qS : ℝ) * ε / (1 - p_abort) ^ 2 + (qS * (qS - 1) / 2) * ε / (1 - p_abort) ^ 2 + = ↑qS * ε * (↑qS + 1) / (2 * (1 - p_abort) ^ 2) := by + field_simp + ring + rw [show (qS : ℝ) * ε / (1 - p_abort) ^ 2 + qS * qH * ε / (1 - p_abort) + + (qS * (qS - 1) / 2) * ε / (1 - p_abort) ^ 2 + = ((qS : ℝ) * ε / (1 - p_abort) ^ 2 + (qS * (qS - 1) / 2) * ε / (1 - p_abort) ^ 2) + + qS * qH * ε / (1 - p_abort) by ring, hkey] + have hextra : (qS : ℝ) * qH * ε / (1 - p_abort) ≤ qS * (qH + 1) * ε / (1 - p_abort) := by + gcongr (?_ / (1 - p_abort)) + nlinarith [mul_nonneg hqS hε, hqS, hqH, hε] + linarith [hextra] + +omit [SampleableType Stmt] in +/-- Hop G₁ → G₂ (Prog → Trans) at a fixed key: dropping the reprogramming of rejected +attempts (keeping only the accepted transcript's programming) costs at most +`qS·(qH+1)·ε/(1-p)`. + +Proof structure: both games are presented as projections of a single ghost-instrumented +run (`ghostHybridImpl`) over the two-layer cache, with rejected-attempt programmings +routed to the ghost layer. Overlaying the ghost layer recovers the Prog game +(`ghostHybridImpl_proj_prog`) and forgetting it recovers the Trans game +(`ghostHybridImpl_proj_trans`) — the deferred-sampling step. The two instrumented +handlers agree until the adversary reads a ghost point +(`tvDist_simulateQ_run_le_probEvent_output_bad`), the verification tail agrees by the +freshness check and the ghost-domain invariant +(`ghostHybridImpl_preserves_signed_inv`), and the firing probability is bounded by the +ghost-read collision charge `probEvent_ghostRead_bad_le`. -/ +lemma probOutput_hybridExpAtKey_prog_le_trans + (qS qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] ≤ + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (transSignBody ids M maxAttempts pk sk) pk] + + ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) := by + classical + set s₀ : ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M := ((∅, ∅), []) with hs₀ + set runP := (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) + (adv.main pk)).run (s₀, false) with hrunP + set runT := (simulateQ (ghostHybridImpl ids M maxAttempts false pk sk) + (adv.main pk)).run (s₀, false) with hrunT + set gP : (M × Option (Commit × Resp)) × GhostState M Commit Chal → ProbComp Bool := + fun z => hybridVerifyCont ids hr M maxAttempts pk + (z.1, (overlayCache M z.2.1.1.1 z.2.1.1.2, z.2.1.2)) with hgP + set gT : (M × Option (Commit × Resp)) × GhostState M Commit Chal → ProbComp Bool := + fun z => hybridVerifyCont ids hr M maxAttempts pk + (z.1, (z.2.1.1.1, z.2.1.2)) with hgT + -- Overlay projection of the instrumented run gives the Prog game. + have hGP : hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk = runP >>= gP := by + rw [hybridExpAtKey_eq_run_bind] + have hproj := OracleComp.map_run_simulateQ_eq_of_query_map_eq + (ghostHybridImpl ids M maxAttempts true pk sk) + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (progSignBody ids M pk sk · maxAttempts)) + (fun g : GhostState M Commit Chal => (overlayCache M g.1.1.1 g.1.1.2, g.1.2)) + (ghostHybridImpl_proj_prog ids M maxAttempts pk sk) + (adv.main pk) (s₀, false) + have hinit : (overlayCache M ((s₀, false) : GhostState M Commit Chal).1.1.1 + (s₀, false).1.1.2, ((s₀, false) : GhostState M Commit Chal).1.2) = + ((∅, []) : (M × Commit →ₒ Chal).QueryCache × List M) := by + simp [hs₀, overlayCache_empty] + rw [hinit] at hproj + rw [← hproj, bind_map_left] + exact bind_congr fun z => rfl + -- Ghost-forgetting projection of the instrumented run gives the Trans game. + have hGT : hybridExpAtKey ids hr M maxAttempts adv + (transSignBody ids M maxAttempts pk sk) pk = runT >>= gT := by + rw [hybridExpAtKey_eq_run_bind] + have hproj := OracleComp.map_run_simulateQ_eq_of_query_map_eq + (ghostHybridImpl ids M maxAttempts false pk sk) + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (transSignBody ids M maxAttempts pk sk)) + (fun g : GhostState M Commit Chal => (g.1.1.1, g.1.2)) + (ghostHybridImpl_proj_trans ids M maxAttempts pk sk) + (adv.main pk) (s₀, false) + have hinit : ((((s₀, false) : GhostState M Commit Chal).1.1.1, + ((s₀, false) : GhostState M Commit Chal).1.2)) = + ((∅, []) : (M × Commit →ₒ Chal).QueryCache × List M) := by + simp [hs₀] + rw [hinit] at hproj + rw [← hproj, bind_map_left] + exact bind_congr fun z => rfl + -- Identical-until-bad on the instrumented runs. + have h_bad := + OracleComp.ProgramLogic.Relational.tvDist_simulateQ_run_le_probEvent_output_bad + (ghostHybridImpl ids M maxAttempts true pk sk) + (ghostHybridImpl ids M maxAttempts false pk sk) + (adv.main pk) s₀ + (ghostHybridImpl_agree_good ids M maxAttempts pk sk) + (ghostHybridImpl_bad_mono ids M maxAttempts true pk sk) + (ghostHybridImpl_bad_mono ids M maxAttempts false pk sk) + set Pbad := Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => + z.2.2 = true | runP] with hPbad + -- Ghost-domain invariant along the Trans-side run. + have h_inv : ∀ z ∈ support runT, + ∀ q : M × Commit, z.2.1.1.2 q ≠ none → q.1 ∈ z.2.1.2 := by + intro z hz + exact OracleComp.simulateQ_run_preserves_inv_of_query + (ghostHybridImpl ids M maxAttempts false pk sk) + (fun g : GhostState M Commit Chal => + ∀ q : M × Commit, g.1.1.2 q ≠ none → q.1 ∈ g.1.2) + (fun t s hs => + ghostHybridImpl_preserves_signed_inv ids M maxAttempts false pk sk t s hs) + (adv.main pk) (s₀, false) (fun q hq => by simp [hs₀] at hq) + z hz + -- The two verification continuations agree on the Trans-side support. + have h_eqT : Pr[= true | runT >>= gP] = Pr[= true | runT >>= gT] := by + rw [probOutput_bind_eq_tsum, probOutput_bind_eq_tsum] + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support runT + · congr 1 + by_cases hmem : z.1.1 ∈ z.2.1.2 + · rw [hgP, hgT] + rw [probOutput_true_hybridVerifyCont_of_mem ids hr M maxAttempts pk + z.1 _ z.2.1.2 hmem, + probOutput_true_hybridVerifyCont_of_mem ids hr M maxAttempts pk + z.1 _ z.2.1.2 hmem] + · have hagree : ∀ w : Commit, + overlayCache M z.2.1.1.1 z.2.1.1.2 (z.1.1, w) = z.2.1.1.1 (z.1.1, w) := by + intro w + refine overlayCache_apply_ghost_none (M := M) _ ?_ + by_contra hne + exact hmem (h_inv z hz (z.1.1, w) hne) + rw [hgP, hgT] + exact congrArg (fun x => Pr[= true | x]) + (hybridVerifyCont_cache_congr ids hr M maxAttempts pk z.1 _ _ z.2.1.2 hagree) + · simp [probOutput_eq_zero_of_not_mem_support hz] + -- Combine: TV budget plus the (open) collision charge. + have h_tv : tvDist (runP >>= gP) (runT >>= gP) ≤ Pbad.toReal := + le_trans (tvDist_bind_right_le gP runP runT) h_bad + have h_badBound : Pbad ≤ ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) := + probEvent_ghostRead_bad_le ids hr M maxAttempts adv qS qH ε p_abort hp₀ hp hε hQ pk sk + hGuess hAbort + have h_real : Pr[= true | runP >>= gP].toReal ≤ + Pr[= true | runT >>= gT].toReal + Pbad.toReal := by + have habs := abs_probOutput_toReal_sub_le_tvDist (runP >>= gP) (runT >>= gP) + have h2 := (abs_le.mp habs).2 + rw [h_eqT] at h2 + linarith [h_tv] + have hPbad_ne_top : Pbad ≠ ⊤ := ne_top_of_le_ne_top ENNReal.one_ne_top probEvent_le_one + calc Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (progSignBody ids M pk sk · maxAttempts) pk] + = Pr[= true | runP >>= gP] := by rw [hGP] + _ = ENNReal.ofReal (Pr[= true | runP >>= gP].toReal) := + (ENNReal.ofReal_toReal probOutput_ne_top).symm + _ ≤ ENNReal.ofReal (Pr[= true | runT >>= gT].toReal + Pbad.toReal) := + ENNReal.ofReal_le_ofReal h_real + _ = Pr[= true | runT >>= gT] + Pbad := by + rw [ENNReal.ofReal_add ENNReal.toReal_nonneg ENNReal.toReal_nonneg, + ENNReal.ofReal_toReal probOutput_ne_top, ENNReal.ofReal_toReal hPbad_ne_top] + _ ≤ Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (transSignBody ids M maxAttempts pk sk) pk] + + ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) := by + rw [hGT] + exact add_le_add le_rfl h_badBound + +omit [SampleableType Stmt] in +/-- Hop G₂ → G₃ (Trans → Sim) at a fixed key: replacing the private honest-execution +loop by the per-attempt HVZK simulator loop costs at most `qS·ζ_zk/(1-p)`. + +Distributional content: per signing query, `transSignBody` and `simSignBody` differ only +in the optional sampler driving `firstSome`; `tvDist_firstSome_le_geometric` bounds the +per-query gap by `ζ_zk · (1 + p + ⋯) ≤ ζ_zk/(1-p)` using `ids.HVZK sim ζ_zk` (`hhvzk`) +and the simulator abort bound (`hAbortSim`), uniformly in the shared starting cache +(`tvDist_run_transSignBody_simSignBody_le`). The per-query total-variation budget is +accumulated across the at-most-`qS` signing queries of the adversary run by +`tvDist_simulateQ_run_le_queryBoundP_mul`, the two hybrid handlers agreeing exactly on +the base (uniform and random-oracle) component. -/ +lemma probOutput_hybridExpAtKey_trans_le_sim + (ζ_zk : ℝ) (hζ : 0 ≤ ζ_zk) (hhvzk : ids.HVZK sim ζ_zk) + (qS qH : ℕ) (p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) + (pk : Stmt) (sk : Wit) (hrel : rel pk sk = true) + (hAbortSim : Pr[= none | sim pk] ≤ ENNReal.ofReal p_abort) : + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (transSignBody ids M maxAttempts pk sk) pk] ≤ + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim pk sk) pk] + + ENNReal.ofReal (qS * ζ_zk / (1 - p_abort)) := by + set ε : ℝ := ζ_zk * ∑ j ∈ Finset.range maxAttempts, p_abort ^ j with hε_def + have hε_nonneg : 0 ≤ ε := + mul_nonneg hζ (Finset.sum_nonneg fun j _ => pow_nonneg hp₀ j) + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + -- The simulator abort bound, in real form. + have hq_toReal : Pr[= none | sim pk].toReal ≤ p_abort := by + have h := ENNReal.toReal_mono ENNReal.ofReal_ne_top hAbortSim + rwa [ENNReal.toReal_ofReal hp₀] at h + -- Per-signing-query step bound, uniform over the hybrid state. + have h_step : ∀ (msg : M) (s : (M × Commit →ₒ Chal).QueryCache × List M), + tvDist ((hybridSignImpl M (transSignBody ids M maxAttempts pk sk) msg).run s) + ((hybridSignImpl M (simSignBody M maxAttempts sim pk sk) msg).run s) ≤ ε := by + intro msg s + have hrun : ∀ (body : M → StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp + (Option (Commit × Resp))), + (hybridSignImpl M body msg).run s = + (fun (ac : Option (Commit × Resp) × (M × Commit →ₒ Chal).QueryCache) => + (ac.1, (ac.2, msg :: s.2))) <$> (body msg).run s.1 := by + intro body + rfl + rw [hrun, hrun] + exact le_trans (tvDist_map_le _ _ _) + (tvDist_run_transSignBody_simSignBody_le ids M maxAttempts sim pk sk hrel msg + hhvzk hq_toReal hp₀ s.1) + -- Accumulate the per-query budget across the `qS` signing queries of the run. + have h_run : tvDist + (StateT.run (simulateQ + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (transSignBody ids M maxAttempts pk sk)) (adv.main pk)) (∅, [])) + (StateT.run (simulateQ + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) (adv.main pk)) (∅, [])) + ≤ qS * ε := by + refine OracleComp.ProgramLogic.Relational.tvDist_simulateQ_run_le_queryBoundP_mul + _ _ hε_nonneg + (· matches .inr _) ?_ ?_ (adv.main pk) (hQ pk).1 (∅, []) + · rintro (t | msg) hSt s + · simp at hSt + · simpa only [OracleSpec.add_apply_inl, OracleSpec.add_apply_inr, + QueryImpl.add_apply_inr] using h_step msg s + · rintro (t | msg) hSt s + · simp only [QueryImpl.add_apply_inl] + · simp at hSt + -- The verification continuation is shared, so the games inherit the run-level bound. + have h_tv_games : tvDist + (hybridExpAtKey ids hr M maxAttempts adv (transSignBody ids M maxAttempts pk sk) pk) + (hybridExpAtKey ids hr M maxAttempts adv (simSignBody M maxAttempts sim pk sk) pk) + ≤ qS * ε := by + refine le_trans ?_ h_run + simp only [hybridExpAtKey] + exact tvDist_bind_right_le _ _ _ + -- Close the finite geometric sum: `∑_{j liftComp oa _`. This collapses the `simulateQ unifSim (firstSome (sim +pk) maxAttempts)` nested simulation in the sign step back to the bare lifted `firstSome` loop — +the part of `hproj2_sign` that is independent of the live-read/sign collision. -/ +lemma simulateQ_unifSim_run {α : Type} + (oa : OracleComp unifSpec α) + (cache : (unifSpec + (M × Commit →ₒ Chal)).QueryCache) : + let spec := unifSpec + (M × Commit →ₒ Chal) + let fwd : QueryImpl spec (StateT spec.QueryCache (OracleComp spec)) := + (HasQuery.toQueryImpl (spec := spec) (m := OracleComp spec)).liftTarget _ + let unifSim : QueryImpl unifSpec (StateT spec.QueryCache (OracleComp spec)) := + fun n => fwd (.inl n) + (simulateQ unifSim oa).run cache = + (fun r => (r, cache)) <$> (liftComp oa (unifSpec + (M × Commit →ₒ Chal))) := by + intro spec fwd unifSim + induction oa using OracleComp.inductionOn generalizing cache with + | pure x => simp [unifSim, fwd] + | query_bind t k ih => + rw [simulateQ_bind, StateT.run_bind, simulateQ_query] + simp only [OracleQuery.input_query, OracleQuery.cont_query, id_map] + -- `unifSim t` forwards the uniform query `t` straight through into the sum spec, leaving + -- the cache untouched. + have hstep : (unifSim t).run cache + = (liftComp (query t : OracleComp unifSpec _) spec) >>= fun u => pure (u, cache) := by + simp only [unifSim, fwd, QueryImpl.liftTarget_apply, HasQuery.toQueryImpl_apply] + change ((liftM (query (Sum.inl t)) : + StateT (unifSpec + (M × Commit →ₒ Chal)).QueryCache + (OracleComp (unifSpec + (M × Commit →ₒ Chal))) _)).run cache = _ + rw [OracleComp.liftM_run_StateT] + refine congrArg (· >>= fun u => pure (u, cache)) ?_ + rfl + rw [hstep, liftComp_bind, map_bind, bind_assoc] + simp only [pure_bind] + exact bind_congr (fun u => ih u cache) + +/-- The inner *managed* handler of the NMA reduction: forward uniform queries to the live +spec (`unifSim`), answer hash queries through the managed cache (`roSim`, forwarding misses +to the live oracle), and answer signing queries with the simulator loop (`sigSim`), programming +the accepted transcript's challenge into the managed cache. This is the +`(unifSim + roSim) + sigSim` handler used inside `simulatedNmaAdv`. -/ +noncomputable def nmaOuterImpl (pk : Stmt) : + QueryImpl.Stateful (unifSpec + (M × Commit →ₒ Chal)) + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (unifSpec + (M × Commit →ₒ Chal)).QueryCache := + letI spec := unifSpec + (M × Commit →ₒ Chal) + letI fwd : QueryImpl spec (StateT spec.QueryCache (OracleComp spec)) := + (HasQuery.toQueryImpl (spec := spec) (m := OracleComp spec)).liftTarget _ + letI unifSim : QueryImpl unifSpec (StateT spec.QueryCache (OracleComp spec)) := + fun n => fwd (.inl n) + letI roSim : QueryImpl (M × Commit →ₒ Chal) + (StateT spec.QueryCache (OracleComp spec)) := fun mc => do + let cache ← get + match cache (.inr mc) with + | some v => pure v + | none => do + let v ← fwd (.inr mc) + modifyGet fun cache => (v, cache.cacheQuery (.inr mc) v) + letI sigSim : QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT spec.QueryCache (OracleComp spec)) := fun msg => do + let r ← simulateQ unifSim (firstSome (sim pk) maxAttempts) + match r with + | some (w, c, z) => + modifyGet fun cache => (some (w, z), cache.cacheQuery (.inr (msg, w)) c) + | none => pure none + (unifSim + roSim) + sigSim + +/-- The outer *runtime* handler of the NMA reduction: forward uniform queries (`unifFwdImpl`) +and answer the residual live random-oracle reads through the runtime's own random oracle +(`randomOracle`), threading the outer cache. This is the +`unifFwdImpl + randomOracle` handler that re-simulates the `.run ∅` boundary in +`simulatedNmaAdv`. -/ +noncomputable def nmaInnerImpl : + QueryImpl.Stateful unifSpec (unifSpec + (M × Commit →ₒ Chal)) + ((M × Commit →ₒ Chal).QueryCache) := + unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) + +/-- The single *linked* handler `nmaOuterImpl pk |>.link nmaInnerImpl` that collapses the +two-layer managed/runtime nesting into one simulation over the product cache +`((unifSpec + (M × Commit →ₒ Chal)).QueryCache × (M × Commit →ₒ Chal).QueryCache)`. +The per-step state-coupling for the NMA bridge is stated against this handler. -/ +noncomputable def nmaLinkImpl (pk : Stmt) : + QueryImpl.Stateful unifSpec + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + ((unifSpec + (M × Commit →ₒ Chal)).QueryCache × (M × Commit →ₒ Chal).QueryCache) := + (nmaOuterImpl M maxAttempts sim pk).link (nmaInnerImpl M) + +/-- The linked-run projection of sub-lemma (b): map the layered ghost-tagged NMA state +`((base, ghost), signed)` onto the linked managed handler's product cache pair. + +The product cache the linked handler `nmaLinkImpl` carries is +`(inner : (unifSpec + (M × Commit →ₒ Chal)).QueryCache, outer : (M × Commit →ₒ Chal).QueryCache)`, +where the **inner** managed cache accumulates *both* live random-oracle reads (`roSim` writes a +fresh value into the inner `.inr mc` slot) *and* the signing-programmed accepted transcripts +(`sigSim` writes `.inr (msg, w) ↦ c`), while the **outer** runtime cache accumulates *only* live +random-oracle reads (`sigSim` never forwards to the outer oracle). + +Hence the consistent per-step projection is: + +* `inner := baseEmbed (overlayCache base ghost)` — the *full* hybrid cache (live reads in the + base layer plus signing-programmed points in the ghost layer), embedded into the sum-keyed + inner cache; and +* `outer := base` — the live-read base layer only. + +This is the corrected projection: an earlier attempt set `inner := baseEmbed base` and +`outer := overlayCache base ghost`, which is inconsistent — the random-oracle step writes a +live read into the inner cache (so it must carry the overlay), while the signing step writes +the programmed transcript into the inner cache and never touches the outer (so the outer must +exclude ghost points). With `inner := baseEmbed (overlay base ghost)` and `outer := base` both +the RO step and the sign step become exact per-step equalities. The signed-message list is +forgotten — the linked handler carries no such list. -/ +def proj2 (s : NmaGhostState M Commit Chal) : + (unifSpec + (M × Commit →ₒ Chal)).QueryCache × (M × Commit →ₒ Chal).QueryCache := + (baseEmbed M (overlayCache M s.1.1 s.1.2), s.1.1) + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), uniform-query step.** On a uniform query the layered ghost-tagged +handler `ghostNmaImpl`, projected by `proj2`, matches the linked managed handler `nmaLinkImpl` +applied to the projected state. The uniform query forwards straight through both handlers +(`unifSim`/`unifFwdImpl`) without touching either cache layer, so the coupling is the +straightforward forward pass. -/ +lemma hproj2_unif (pk : Stmt) (sk : Wit) (n : unifSpec.Domain) + (s : NmaGhostState M Commit Chal) : + Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inl n))).run s = + (nmaLinkImpl M maxAttempts sim pk (.inl (.inl n))).run (proj2 M s) := by + rw [ghostNmaImpl_run_unif, nmaLinkImpl, QueryImpl.Stateful.link_impl_apply_run] + simp only [nmaOuterImpl, QueryImpl.add_apply_inl, QueryImpl.liftTarget_apply, + HasQuery.toQueryImpl_apply, nmaInnerImpl, unifFwdImpl, proj2] + rfl + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), random-oracle step — cached-read sub-case.** On a random-oracle query at a +point `mc` whose ghost layer misses (`hgm`) and whose base layer already holds a value `v` +(`hbh : s.1.1 mc = some v`), the layered ghost-tagged handler `ghostNmaImpl`, projected by +`proj2`, matches the linked managed handler `nmaLinkImpl` applied to the projected state. Both +sides read the cached value: `ghostNmaImpl` returns `roStep`'s cached branch (base hit), while +the linked `roSim` finds the same value in the inner managed cache (`baseEmbed base`, which holds +the base entry at `.inr mc`) and short-circuits, so neither cache layer is written. + +The two other RO sub-cases are handled by the siblings `hproj2_ro_fresh` and +`hproj2_ro_ghost_hit`: + +* **fresh live read** (`s.1.1 mc = none`, ghost miss, `hproj2_ro_fresh`): the read resamples; both + sides write the sampled value to base/inner (`baseEmbed_cacheQuery`) and to overlay/outer + (`overlayCache_cacheQuery_real_of_ghost_none`, via the `randomOracle_run_eq_roStep` round-trip), + reducing to a `roStep`-on-`overlayCache` match under the inner-`roSim` / outer-`randomOracle` + nested-simulation `.run` plumbing. +* **ghost hit** (`s.1.2 mc ≠ none`, `hproj2_ro_ghost_hit`): `ghostNmaImpl` returns the ghost value + leaving its state untouched, whereas the linked `roSim` re-reads through the runtime + `randomOracle`, recovering the same value from `overlayCache base ghost`. Because `proj2` places + the *overlay* in the inner slot and only the base layer in the outer slot, the re-cached point is + already present on the projected side, so the step is an equality without needing a + reachable-state side condition. -/ +lemma hproj2_ro (pk : Stmt) (sk : Wit) (mc : M × Commit) (v : Chal) + (s : NmaGhostState M Commit Chal) (hgm : s.1.2 mc = none) (hbh : s.1.1 mc = some v) : + Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inr mc))).run s = + (nmaLinkImpl M maxAttempts sim pk (.inl (.inr mc))).run (proj2 M s) := by + rw [ghostNmaImpl_run_ro, nmaLinkImpl, QueryImpl.Stateful.link_impl_apply_run] + simp only [nmaOuterImpl, QueryImpl.add_apply_inl, QueryImpl.add_apply_inr, proj2] + rw [hgm] + -- The inner managed cache is now `baseEmbed (overlayCache base ghost)`; since the ghost layer + -- misses at `mc` (`hgm`), the overlay agrees with the base layer there (`= some v`), so `roSim` + -- finds the value in the inner cache and short-circuits without touching either cache layer. + have hov : overlayCache M s.1.1 s.1.2 mc = some v := + (overlayCache_apply_ghost_none (M := M) s.1.1 hgm).trans hbh + erw [StateT.run_bind, StateT.run_get] + simp only [pure_bind, baseEmbed_inr, hov, roStep_of_some M hbh, map_pure, nmaInnerImpl] + erw [StateT.run_pure] + simp only [map_pure, QueryImpl.Stateful.Frame.linkReshape, QueryImpl.Stateful.Frame.prod, + PFunctor.Lens.State.fst, PFunctor.Lens.State.snd, Prod.map, id_eq, proj2] + rfl + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), random-oracle step — ghost-hit sub-case.** On a random-oracle query at a +point `mc` whose ghost layer already holds a value `v` (`hgh : s.1.2 mc = some v`), the layered +ghost-tagged handler `ghostNmaImpl`, projected by `proj2`, matches the linked managed handler +`nmaLinkImpl` applied to the projected state. `ghostNmaImpl` returns the ghost value leaving its +state untouched; under `proj2` the inner managed cache is `baseEmbed (overlayCache base ghost)`, +which carries the ghost value at `.inr mc` (`overlayCache_apply_ghost_some`), so the linked +`roSim` finds it and short-circuits without touching either layer. Placing the overlay — rather +than the bare base layer — in the inner slot is what makes this sub-case an exact equality, with +no reachability side condition on the state. -/ +lemma hproj2_ro_ghost_hit (pk : Stmt) (sk : Wit) (mc : M × Commit) (v : Chal) + (s : NmaGhostState M Commit Chal) (hgh : s.1.2 mc = some v) : + Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inr mc))).run s = + (nmaLinkImpl M maxAttempts sim pk (.inl (.inr mc))).run (proj2 M s) := by + rw [ghostNmaImpl_run_ro, nmaLinkImpl, QueryImpl.Stateful.link_impl_apply_run] + simp only [nmaOuterImpl, QueryImpl.add_apply_inl, QueryImpl.add_apply_inr, proj2] + rw [hgh] + -- The overlay holds the ghost value at `mc`, so the inner managed cache `baseEmbed (overlay + -- base ghost)` does too; `roSim` short-circuits. + have hov : overlayCache M s.1.1 s.1.2 mc = some v := + overlayCache_apply_ghost_some (M := M) s.1.1 hgh + erw [StateT.run_bind, StateT.run_get] + simp only [pure_bind, baseEmbed_inr, hov, map_pure, nmaInnerImpl] + erw [StateT.run_pure] + simp only [map_pure, QueryImpl.Stateful.Frame.linkReshape, QueryImpl.Stateful.Frame.prod, + PFunctor.Lens.State.fst, PFunctor.Lens.State.snd, Prod.map, id_eq, proj2] + rfl + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), random-oracle step — fresh-live-read sub-case.** On a random-oracle +query at a point `mc` whose ghost layer misses (`hgm`) and whose base layer also misses +(`hbm : s.1.1 mc = none`), the layered ghost-tagged handler `ghostNmaImpl`, projected by +`proj2`, matches the linked managed handler `nmaLinkImpl` applied to the projected state. +Both sides resample a fresh value `c`; `ghostNmaImpl` writes it to the base layer (`roStep`'s +miss branch), while the linked `roSim` misses the inner managed cache (`baseEmbed base`, +which has no entry at `.inr mc` since `base mc = none`) and forwards to the runtime +`randomOracle` (the `randomOracle_run_eq_roStep` round-trip), caching the result both in the +inner managed cache and the outer runtime cache. Under `proj2`, the inner write matches +`baseEmbed_cacheQuery` and the outer write matches `overlayCache_cacheQuery_real_of_ghost_none`. -/ +lemma hproj2_ro_fresh (pk : Stmt) (sk : Wit) (mc : M × Commit) + (s : NmaGhostState M Commit Chal) (hgm : s.1.2 mc = none) (hbm : s.1.1 mc = none) : + Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk (.inl (.inr mc))).run s = + (nmaLinkImpl M maxAttempts sim pk (.inl (.inr mc))).run (proj2 M s) := by + rw [ghostNmaImpl_run_ro, nmaLinkImpl, QueryImpl.Stateful.link_impl_apply_run] + simp only [nmaOuterImpl, QueryImpl.add_apply_inl, QueryImpl.add_apply_inr, proj2] + rw [hgm] + -- The inner managed cache is now `baseEmbed (overlayCache base ghost)`; since both the ghost + -- layer (`hgm`) and the base layer (`hbm`) miss at `mc`, the overlay misses there too, so + -- `roSim`'s inner lookup misses and forwards to the outer runtime `randomOracle`. + have hov : overlayCache M s.1.1 s.1.2 mc = none := by simp [overlayCache, hgm, hbm] + erw [StateT.run_bind, StateT.run_get] + simp only [pure_bind, baseEmbed_inr, hov] + rw [QueryImpl.liftTarget_apply, HasQuery.toQueryImpl_apply] + -- Reduce the inner `roSim` body run to a single `query` followed by an inner-cache write, + -- then push `simulateQ nmaInnerImpl` through it: the `.inr mc` query is answered by the + -- runtime `randomOracle`, whose run is `roStep` on the outer cache. + conv_rhs => + enter [2, 1, 2] + change (query (Sum.inr mc) : OracleComp (unifSpec + (M × Commit →ₒ Chal)) _) >>= + fun v => pure (v, (baseEmbed M (overlayCache M s.1.1 s.1.2)).cacheQuery (Sum.inr mc) v) + rw [simulateQ_bind] + simp only [simulateQ_pure] + conv_rhs => + enter [2, 1, 1] + rw [show (query (Sum.inr mc) : OracleComp (unifSpec + (M × Commit →ₒ Chal)) _) = + liftM ((unifSpec + (M × Commit →ₒ Chal)).query (Sum.inr mc)) from rfl, + simulateQ_spec_query] + simp only [nmaInnerImpl, QueryImpl.add_apply_inr] + rw [StateT.run_bind] + conv_rhs => enter [2, 1]; erw [randomOracle_run_eq_roStep] + -- Both sides resample: the layered run's base layer and the linked run's outer cache (now the + -- base layer too) both miss at `mc`. + rw [roStep_of_none M hbm] + -- Normalise both sides to a single resample, mapping `c` to a `(c, inner, outer)` triple. + simp only [bind_pure_comp, StateT.run_pure] + conv_lhs => erw [Functor.map_map, Functor.map_map] + conv_rhs => erw [Functor.map_map, Functor.map_map] + refine map_congr fun c => ?_ + -- Reconcile the cache writes on the two layers: the inner write matches `baseEmbed`'s + -- `cacheQuery`, the outer write matches the overlay's `cacheQuery` (ghost misses at `mc`). + simp only [Prod.map, id_eq, proj2, QueryImpl.Stateful.Frame.linkReshape, + QueryImpl.Stateful.Frame.prod, PFunctor.Lens.State.fst, PFunctor.Lens.State.snd, + PFunctor.Lens.State.put, PFunctor.Lens.State.mk] + rw [overlayCache_cacheQuery_real_of_ghost_none (M := M) s.1.1 hgm, baseEmbed_cacheQuery] + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), signing-query step — exact per-step equality.** + +On a signing query, the layered ghost-tagged handler `ghostNmaImpl`, projected by the +*redesigned* `proj2 ((base, ghost), signed) = (baseEmbed (overlayCache base ghost), base)`, +equals the linked managed handler `nmaLinkImpl` applied to the projected state — *unconditionally* +(no collision hypothesis). + +The redesign is what makes this exact. The linked managed `sigSim` writes the accepted transcript +into the *inner managed cache* (`cacheQuery (.inr (msg, w)) c`) and leaves the *outer runtime +cache* untouched. The layered run writes the same transcript into the *ghost layer*, leaving the +base layer untouched. Under the new `proj2`, the inner managed cache is recovered as `baseEmbed` +of the *full overlay* `overlayCache base ghost`, so the ghost-layer write surfaces in `proj2`'s +*first* slot exactly where `sigSim` writes (`overlayCache_cacheQuery_ghost` then +`baseEmbed_cacheQuery`), while the outer cache is `proj2`'s *second* slot `base`, untouched on both +sides. There is no slot swap and no dependence on whether `(msg, w)` coincides with a prior live +read: `proj2`'s first component carries the full overlay, so the sign point lands in the same inner +slot regardless. (This supersedes the earlier `proj2 = (baseEmbed base, overlayCache base ghost)` +projection, for which this step was provably *not* a per-step state function.) + +PROOF SHAPE. `link_impl_apply_run` exposes the linked RHS as the nested simulation +`simulateQ nmaInnerImpl ((nmaOuterImpl pk (.inr msg)).run outerCache)`; `simp [nmaOuterImpl]` +reduces the outer step to the `sigSim` body — a nested `simulateQ unifSim (firstSome (sim pk) +maxAttempts)` (collapsed by `simulateQ_unifSim_run`, the simulator loop touches no cache layer) +followed by inner-cache programming `cacheQuery (.inr (msg, w)) c`. The LHS is `simGhostSignBody` +(`liftM (firstSome (sim pk) maxAttempts)` then ghost-layer `cacheQuery (msg, w) c`). A +support-restricted `SPMF` bind congruence on the accepted transcript then reduces both sides to +matching pure values, closed by the overlay/`baseEmbed` cache algebra above. -/ +lemma hproj2_sign (pk : Stmt) (sk : Wit) (msg : M) + (s : NmaGhostState M Commit Chal) : + 𝒟[Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk (.inr msg)).run s] = + 𝒟[(nmaLinkImpl M maxAttempts sim pk (.inr msg)).run (proj2 M s)] := by + rw [ghostNmaImpl_run_sign, nmaLinkImpl, QueryImpl.Stateful.link_impl_apply_run] + -- Reduce the linked RHS's outer step `nmaOuterImpl pk (.inr msg)` to the simulator body + -- `sigSim msg`: a nested simulation `simulateQ unifSim (firstSome (sim pk) maxAttempts)` + -- followed by inner-cache programming of the accepted transcript. After this the residual is + -- the uniform-only nested-simulation collapse (i) above; the per-step equality then fails + -- exactly on `signLiveCollision`, which the leaf's collision-accounting reframe pays on the + -- bad side rather than discharging here. + simp only [nmaOuterImpl, QueryImpl.add_apply_inr] + -- Reduce the LHS to `firstSome (sim pk) maxAttempts >>= ghostSignProgramCont`. + simp only [simGhostSignBody, StateT.run_bind, OracleComp.liftM_run_StateT, bind_assoc, + pure_bind, map_bind] + -- Collapse the RHS's nested `simulateQ unifSim (firstSome …)` loop via `simulateQ_unifSim_run`. + conv_rhs => enter [1, 2, 1]; erw [StateT.run_bind]; rw [simulateQ_unifSim_run] + -- Distribute the outer `simulateQ nmaInnerImpl` and `.run` over the bind, and collapse the + -- lifted `firstSome` loop against `nmaInnerImpl`'s uniform-forwarding branch (`roSim`). + rw [simulateQ_bind, StateT.run_bind, simulateQ_map, StateT.run_map] + rw [show nmaInnerImpl M = unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) from rfl, + roSim.simulateQ_liftComp, unifFwdImpl.simulateQ_run] + -- Both sides are now `firstSome … >>= (per-output programming)`; align by support-restricted + -- bind congruence and case-split on the accepted transcript. + simp only [map_bind, Functor.map_map, bind_map_left] + -- Move into `SPMF` (where the map/bind laws apply cleanly, dodging the `OracleComp` `Functor` + -- vs `Monad` map friction): both sides become `𝒟[firstSome …] >>= (per-output programming)`. + simp only [evalDist_map, evalDist_bind] + conv_lhs => enter [2]; erw [evalDist_bind] + rw [map_bind] + -- Support-restricted `SPMF` bind congruence (`evalDist_bind_congr` with `m := SPMF`, where + -- `evalDist` is the identity): case-split on the accepted transcript, using the no-collision + -- hypothesis on the `some` branch. + refine evalDist_bind_congr (m := SPMF) (mx := 𝒟[firstSome (sim pk) maxAttempts]) + fun a _ha => ?_ + simp only [SPMF.evalDist_def] + -- Case-split on the accepted transcript; under the redesigned `proj2` the `some` branch aligns + -- the ghost-layer write with the inner-cache write unconditionally (no collision hypothesis). + cases a with + | none => + -- The all-abort outcome programs no point on either side; both reduce to `(none, proj2 s)`. + simp only [ghostSignProgramCont, StateT.run_pure] + conv_rhs => enter [2, 1, 1, 2]; erw [StateT.run_pure] + conv_rhs => enter [2, 1, 1]; rw [simulateQ_pure] + conv_rhs => enter [2, 1]; erw [StateT.run_pure] + simp only [evalDist_pure, map_pure, proj2, QueryImpl.Stateful.Frame.linkReshape, + QueryImpl.Stateful.Frame.prod, PFunctor.Lens.State.fst, PFunctor.Lens.State.snd, + PFunctor.Lens.State.put, PFunctor.Lens.State.mk] + conv_lhs => erw [evalDist_pure]; rw [map_pure] + simp only [proj2, Prod.map, id_eq] + | some wcz => + obtain ⟨w, c, z⟩ := wcz + -- Reduce the LHS ghost-layer programming to a pure value. + simp only [ghostSignProgramCont, StateT.run_bind, StateT.run_modify, pure_bind, + StateT.run_pure, map_pure] + -- Reduce the RHS inner-cache programming and the trivial outer simulation to a pure value. + conv_rhs => enter [2, 1, 1, 2]; erw [StateT.run_modifyGet] + rw [simulateQ_pure] + erw [StateT.run_pure] + simp only [evalDist_pure, map_pure, proj2, QueryImpl.Stateful.Frame.linkReshape, + QueryImpl.Stateful.Frame.prod, PFunctor.Lens.State.fst, PFunctor.Lens.State.snd, + PFunctor.Lens.State.put, PFunctor.Lens.State.mk] + conv_lhs => erw [evalDist_pure]; rw [map_pure] + simp only [proj2, Prod.map, id_eq] + -- Off the collision (`hbase : s.1.1 (msg, w) = none`) the two pure values agree exactly under + -- the *redesigned* `proj2 ((base, ghost), signed) = (baseEmbed (overlay base ghost), base)`. + -- Both sides write the accepted transcript into the inner managed cache and leave the outer + -- (live-read) cache `base` untouched: + -- LHS = (some (w, z), baseEmbed (overlay base (ghost.cacheQuery (msg, w) c)), base) + -- RHS = (some (w, z), (baseEmbed (overlay base ghost)).cacheQuery (.inr (msg, w)) c, base). + -- The ghost-layer write surfaces in the inner cache (`proj2`'s *first* slot, via the overlay) + -- exactly where the linked `sigSim` writes `.inr (msg, w) ↦ c`, and the live-read layer + -- `base` (`proj2`'s *second* slot = the linked outer cache) is untouched on both sides — so + -- the per-step sign equality is now exact (no slot swap). The `hbase` no-collision hypothesis + -- is not even needed for the cache algebra under the redesigned projection: `proj2`'s first + -- component carries the full overlay, so the sign point lands in the same inner slot whether + -- or not it coincides with a prior live read. + rw [overlayCache_cacheQuery_ghost, baseEmbed_cacheQuery] + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), unified per-step `evalDist` coupling.** For *every* oracle query `t` and +every layered state `s`, the `proj2`-projected layered NMA step has the same output/state +distribution as the linked managed step on the projected state. This bundles the four per-step +lemmas (`hproj2_unif`, `hproj2_ro`/`hproj2_ro_ghost_hit`/`hproj2_ro_fresh`, `hproj2_sign`): under +the redesigned `proj2 ((base, ghost), signed) = (baseEmbed (overlayCache base ghost), base)` each +step is an exact equality (the random-oracle and signing steps no longer depend on any reachability +or no-collision side condition), so the coupling holds unconditionally on all of `t`. This is the +per-query hypothesis for the whole-run state-projection `relTriple_simulateQ_run`. -/ +lemma hproj2_evalDist (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : NmaGhostState M Commit Chal) : + 𝒟[Prod.map id (proj2 M) <$> (ghostNmaImpl M maxAttempts sim pk sk t).run s] = + 𝒟[(nmaLinkImpl M maxAttempts sim pk t).run (proj2 M s)] := by + rcases t with (n | mc) | msg + · exact congrArg _ (hproj2_unif M maxAttempts sim pk sk n s) + · rcases hgh : s.1.2 mc with _ | v + · rcases hbh : s.1.1 mc with _ | w + · exact congrArg _ (hproj2_ro_fresh M maxAttempts sim pk sk mc s hgh hbh) + · exact congrArg _ (hproj2_ro M maxAttempts sim pk sk mc w s hgh hbh) + · exact congrArg _ (hproj2_ro_ghost_hit M maxAttempts sim pk sk mc v s hgh) + · exact hproj2_sign M maxAttempts sim pk sk msg s + +/-- **Graph coupling along a function.** If pushing `oa` forward through `F` matches `ob` in +distribution, then `oa` and `ob` are related (as a `RelTriple`) by the graph relation +`fun a b => F a = b`. This is the reverse direction of `evalDist_map_eq_of_relTriple`: the +witnessing coupling is the deterministic coupling `𝒟[oa] >>= fun a => pure (a, F a)`, whose first +marginal is `𝒟[oa]` and whose second marginal is `𝒟[F <$> oa] = 𝒟[ob]`, supported on the graph. -/ +private lemma relTriple_graph_of_evalDist_map_eq + {ι₁ ι₂ : Type} {spec₁ : OracleSpec ι₁} {spec₂ : OracleSpec ι₂} + [IsUniformSpec spec₁] [IsUniformSpec spec₂] + {α' σ' : Type} (F : α' → σ') + (oa : OracleComp spec₁ α') (ob : OracleComp spec₂ σ') + (h : 𝒟[F <$> oa] = 𝒟[ob]) : + OracleComp.ProgramLogic.Relational.RelTriple oa ob (fun a b => F a = b) := by + apply (OracleComp.ProgramLogic.Relational.relTriple_iff_relWP + (oa := oa) (ob := ob) (R := fun a b => F a = b)).2 + refine ⟨⟨𝒟[oa] >>= fun a => pure (a, F a), ?_, ?_⟩, ?_⟩ + · rw [map_bind]; simp + · rw [← h, evalDist_map, map_bind]; simp + · intro z hz + rcases (mem_support_bind_iff + (𝒟[oa]) (fun a => (pure (a, F a) : SPMF (α' × σ'))) z).1 hz with ⟨a, _, hz'⟩ + have hzEq : z = (a, F a) := by + simpa [support_pure, Set.mem_singleton_iff] using hz' + simp [hzEq] + +omit [SampleableType Stmt] in +/-- **Sub-lemma (b), whole-run state projection.** The full layered ghost-tagged NMA run +`(simulateQ ghostNmaImpl (adv.main pk)).run s`, projected by `proj2`, has the same output/state +distribution as the linked managed run `(simulateQ nmaLinkImpl (adv.main pk)).run (proj2 s)`. This +lifts the per-step coupling `hproj2_evalDist` through `relTriple_simulateQ_run` with the state +relation `R s' p := proj2 s' = p` (output-equal, `proj2`-related states), the per-step `RelTriple` +being recovered from the per-step `evalDist`-map equality by the graph coupling +`relTriple_graph_of_evalDist_map_eq`. -/ +lemma evalDist_map_run_simulateQ_ghostNmaImpl_proj2 {β : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))) β) + (s : NmaGhostState M Commit Chal) : + 𝒟[Prod.map id (proj2 M) <$> (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) oa).run s] = + 𝒟[(simulateQ (nmaLinkImpl M maxAttempts sim pk) oa).run (proj2 M s)] := by + -- State relation: `s'` and `p` are related iff `p` is the `proj2`-projection of `s'`. + have hrel := OracleComp.ProgramLogic.Relational.relTriple_simulateQ_run + (impl₁ := ghostNmaImpl M maxAttempts sim pk sk) + (impl₂ := nmaLinkImpl M maxAttempts sim pk) + (R_state := fun (s' : NmaGhostState M Commit Chal) p => proj2 M s' = p) + (oa := oa) + (himpl := fun t s₁ s₂ hs => ?_) + (s₁ := s) (s₂ := proj2 M s) rfl + · -- The whole-run `RelTriple` carries `p₁.1 = p₂.1 ∧ proj2 p₁.2 = p₂.2`, i.e. the graph of + -- `Prod.map id proj2`. Re-express it as a graph relation and extract the `map`-equality. + have hrel' : OracleComp.ProgramLogic.Relational.RelTriple + ((simulateQ (ghostNmaImpl M maxAttempts sim pk sk) oa).run s) + ((simulateQ (nmaLinkImpl M maxAttempts sim pk) oa).run (proj2 M s)) + (fun p₁ p₂ => Prod.map id (proj2 M) p₁ = p₂) := + OracleComp.ProgramLogic.Relational.relTriple_post_mono hrel + (fun p₁ p₂ ⟨h1, h2⟩ => Prod.ext h1 h2) + have := OracleComp.ProgramLogic.Relational.evalDist_map_eq_of_relTriple + (f := Prod.map id (proj2 M)) (g := id) hrel' + simpa using this + · -- Per-step coupling from the unified per-step `evalDist`-map equality, via the graph coupling. + subst hs + refine OracleComp.ProgramLogic.Relational.relTriple_post_mono + (relTriple_graph_of_evalDist_map_eq (F := Prod.map id (proj2 M)) + ((ghostNmaImpl M maxAttempts sim pk sk t).run s₁) + ((nmaLinkImpl M maxAttempts sim pk t).run (proj2 M s₁)) + (hproj2_evalDist M maxAttempts sim pk sk t s₁)) ?_ + rintro p₁ p₂ rfl + exact ⟨rfl, rfl⟩ + + +/-- The managed-RO NMA reduction for Fiat-Shamir with aborts: run the CMA adversary, +forwarding uniform queries, answering live hash queries through a managed cache, and +answering signing queries with the simulator loop of `simSignBody` (programming the +accepted transcript's challenge into the managed cache). Returns the forgery together +with the managed cache, in the interface of `SignatureAlg.managedRoNmaAdv`. -/ +noncomputable def simulatedNmaAdv : + SignatureAlg.managedRoNmaAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts) where + main pk := + let spec := unifSpec + (M × Commit →ₒ Chal) + let fwd : QueryImpl spec (StateT spec.QueryCache (OracleComp spec)) := + (HasQuery.toQueryImpl (spec := spec) (m := OracleComp spec)).liftTarget _ + let unifSim : QueryImpl unifSpec (StateT spec.QueryCache (OracleComp spec)) := + fun n => fwd (.inl n) + let roSim : QueryImpl (M × Commit →ₒ Chal) + (StateT spec.QueryCache (OracleComp spec)) := fun mc => do + let cache ← get + match cache (.inr mc) with + | some v => pure v + | none => do + let v ← fwd (.inr mc) + modifyGet fun cache => (v, cache.cacheQuery (.inr mc) v) + let sigSim : QueryImpl (M →ₒ Option (Commit × Resp)) + (StateT spec.QueryCache (OracleComp spec)) := fun msg => do + let r ← simulateQ unifSim (firstSome (sim pk) maxAttempts) + match r with + | some (w, c, z) => + modifyGet fun cache => (some (w, z), cache.cacheQuery (.inr (msg, w)) c) + | none => pure none + -- Run the inner CMA adversary under the managed simulation, then erase the + -- forgery's own verification point from the returned cache (Option B). The + -- with-aborts `verify pk msg (some (w', z))` issues exactly one hash query, at + -- `(msg, w')`; clearing that entry makes `withCacheOverlay advCache verify` miss + -- there and fall through to the live oracle, so the managed-RO experiment agrees + -- with the plain EUF-NMA verification on *every* forgery. In particular a replayed + -- signed `(msg, w')` no longer wins through the programmed challenge, which is what + -- makes the bridge to `eufNmaAdv.advantage` sound. Other programmed entries sit at + -- different points and are never read by `verify`. + (simulateQ ((unifSim + roSim) + sigSim) (adv.main pk)).run ∅ >>= fun result => + let ((msg, σ), cache) := result + let advCache : spec.QueryCache := + match σ with + | some (w', _) => Function.update cache (Sum.inr (msg, w')) none + | none => cache + pure ((msg, σ), advCache) + +omit [SampleableType Stmt] in +/-- **Nested-simulation fusion for the managed NMA run.** The managed reduction runs the +common adversary `adv.main pk` under the inner managed handler `nmaOuterImpl pk` threading the +inner cache (`StateT spec.QueryCache (OracleComp spec)`), then `.run ∅` re-simulates the +residual live queries under the outer runtime handler `nmaInnerImpl` (`unifFwdImpl + +randomOracle`) threading the outer cache. By `QueryImpl.Stateful.simulateQ_link_run` this +two-layer nesting is a single simulation of the *linked* handler `nmaLinkImpl pk = +(nmaOuterImpl pk).link nmaInnerImpl` over the product cache, up to the canonical `linkReshape` +regrouping of the final state. This collapses the explicit `.run ∅` boundary into a single +`simulateQ` whose state is the genuine `(inner managed cache, outer runtime cache)` pair the +per-step coupling projects onto. -/ +lemma managedRun_eq_link_run (pk : Stmt) : + letI spec := unifSpec + (M × Commit →ₒ Chal) + (simulateQ (nmaLinkImpl M maxAttempts sim pk) (adv.main pk)).run (∅, ∅) = + (QueryImpl.Stateful.Frame.prod spec.QueryCache + ((M × Commit →ₒ Chal).QueryCache)).linkReshape (∅, ∅) <$> + (simulateQ (nmaInnerImpl M) + ((simulateQ (nmaOuterImpl M maxAttempts sim pk) + (adv.main pk)).run ∅)).run ∅ := by + exact (QueryImpl.Stateful.simulateQ_link_run _ _ (adv.main pk) ∅ ∅) + +omit [SampleableType Stmt] [SampleableType Chal] in +/-- If a cache misses at the forgery's verification point `Sum.inr (msg, w')`, the overlay +verification of `FiatShamirWithAbort.verify pk msg (some (w', z))` agrees with the plain +live verification: the single query at `Sum.inr (msg, w')` misses and is forwarded live. +The `none` case is verification-free, so it is trivially overlay-insensitive. -/ +lemma withCacheOverlay_verify_eq_of_miss + (cache : (unifSpec + (M × Commit →ₒ Chal)).QueryCache) (pk : Stmt) + (msg : M) (σ : Option (Commit × Resp)) + (hmiss : ∀ w' z, σ = some (w', z) → cache (Sum.inr (msg, w')) = none) : + withCacheOverlay cache + ((FiatShamirWithAbort (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).verify pk msg σ) = + (FiatShamirWithAbort (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) + ids hr M maxAttempts).verify pk msg σ := by + cases σ with + | none => simp only [FiatShamirWithAbort, withCacheOverlay_pure] + | some wz => + obtain ⟨w', z⟩ := wz + have hm : cache (Sum.inr (msg, w')) = none := hmiss w' z rfl + change withCacheOverlay _ + ((query (Sum.inr (msg, w')) : + OracleComp (unifSpec + (M × Commit →ₒ Chal)) + ((unifSpec + (M × Commit →ₒ Chal)).Range (Sum.inr (msg, w')))) >>= + fun c => pure (ids.verify pk w' c z)) = + (query (Sum.inr (msg, w')) : + OracleComp (unifSpec + (M × Commit →ₒ Chal)) + ((unifSpec + (M × Commit →ₒ Chal)).Range (Sum.inr (msg, w')))) >>= + fun c => pure (ids.verify pk w' c z) + rw [withCacheOverlay_bind_pure, bind_pure_comp] + congr 1 + exact withCacheOverlay_query_miss _ (Sum.inr (msg, w')) hm + +omit [SampleableType Stmt] in +/-- **Verify-tail pointwise split** (the per-forgery content of the NMA bridge). On a common +ghost-tagged output state `((base, ghost), signed)` satisfying the ghost-domain invariant +(every ghost point's message is signed), the hybrid verification-and-freshness continuation +`hybridVerifyCont` on the overlay cache is bounded by the managed overlay verification on the +base cache. On `msg ∈ signed` the freshness conjunct zeroes the left +(`probOutput_true_hybridVerifyCont_of_mem`); on a fresh forgery `msg ∉ signed` the invariant +makes the ghost layer miss at every `(msg, w)`, so the overlay agrees with the base cache +(`hybridVerifyCont_cache_congr`), the Option-B post-processing makes `withCacheOverlay` miss +its own verification point (`withCacheOverlay_verify_eq_of_miss`), and the two tails coincide. -/ +lemma probOutput_hybridVerifyCont_le_managed_verify (pk : Stmt) + (ms : M × Option (Commit × Resp)) (base ghost : (M × Commit →ₒ Chal).QueryCache) + (signed : List M) + (hinv : ∀ q : M × Commit, ghost q ≠ none → q.1 ∈ signed) : + Pr[= true | hybridVerifyCont ids hr M maxAttempts pk + (ms, (overlayCache M base ghost, signed))] ≤ + Pr[= true | (fun x : Bool × _ => x.1) <$> + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (withCacheOverlay + (match ms.2 with + | some (w', _) => Function.update (baseEmbed M (overlayCache M base ghost)) + (Sum.inr (ms.1, w')) none + | none => baseEmbed M (overlayCache M base ghost)) + ((FiatShamirWithAbort ids hr M maxAttempts).verify pk ms.1 ms.2))).run base] := by + obtain ⟨msg, σ⟩ := ms + by_cases hmem : msg ∈ signed + · rw [probOutput_true_hybridVerifyCont_of_mem ids hr M maxAttempts pk (msg, σ) + (overlayCache M base ghost) signed hmem] + exact zero_le + · rw [withCacheOverlay_verify_eq_of_miss ids hr M maxAttempts _ pk msg σ + (by intro w' z hσ; simp [hσ]), + hybridVerifyCont_cache_congr ids hr M maxAttempts pk (msg, σ) + (overlayCache M base ghost) base signed + (fun w => overlayCache_apply_ghost_none (M := M) base + (by by_contra h; exact hmem (hinv (msg, w) h)))] + refine le_of_eq ?_ + simp only [hybridVerifyCont, hmem, not_false_eq_true, decide_true, Bool.true_and, + StateT.run', bind_pure] + rfl + +omit [SampleableType Stmt] in +/-- **State-coupling for the NMA bridge** (genuine two-layer content). At a fixed key pair +the single-cache hybrid run of `hybridExpAtKey`, *followed by its verification-and-freshness +tail* `hybridVerifyCont`, is bounded by the run-normal-form of the managed-RO NMA +experiment: the managed-cache run of `simulatedNmaAdv` (re-simulated under the runtime's +outer `randomOracle`), followed by overlay verification. + +The two presentations run the *same* adversary `adv.main pk` but thread the random-oracle +cache through genuinely different layers: + +* the **hybrid** (`impl₁ := hybridBaseImpl + hybridSignImpl simSignBody`) keeps a *single* + cache `(cache, signed)`, into which both live RO reads (`randomOracle`) and the signing + simulation's accepted-transcript programming (`simSignBody` via `signProgramCont`) write; +* the **managed reduction** (`simulatedNmaAdv.main`) keeps an *inner managed* cache threaded + by `roSim`/`sigSim`, whose live `fwd` reads are resolved by the runtime's *separate outer* + `randomOracle` cache. `simulateQ_compose` (`∘ₛ`) does not collapse these two layers because + the inner `.run ∅` boundary turns `roSim`/`fwd` misses into live queries answered by the + outer oracle. + +The coupling claim is that the *overlay* of the inner managed cache onto the outer runtime +cache reproduces the single hybrid cache throughout the run (a state-projection in the sense +of `OracleComp.map_run_simulateQ_eq_of_query_map_eq_inv'`), and that the signed-message list +matches the set of points the managed simulation programmed (a cache invariant in the style +of `fsAbortSignLoop_cache_invariant`). On `msg ∈ signed` the freshness conjunct kills the +left side (`probOutput_true_hybridVerifyCont_of_mem`); on fresh forgeries the +`withCacheOverlay` verification agrees with the live verification at the verification point +(`withCacheOverlay_verify_eq_of_miss`, since the managed point at `(msg, w')` carries the +programmed challenge that equals the hybrid's cached value, while the freshness check rules +out a stale read). Hence the per-forgery success of the hybrid tail is at most that of the +overlay verification, and the bound follows. -/ +lemma hybridSimRun_le_managedRun_verify (pk : Stmt) (sk : Wit) : + Pr[= true | (simulateQ + (hybridBaseImpl (Commit := Commit) (Chal := Chal) M + + hybridSignImpl M (simSignBody M maxAttempts sim pk sk)) + (adv.main pk)).run (∅, []) >>= hybridVerifyCont ids hr M maxAttempts pk] ≤ + Pr[= true | (fun x : Bool × _ => x.1) <$> do + let p ← (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + ((simulatedNmaAdv ids hr M maxAttempts sim adv).main pk)).run ∅ + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (withCacheOverlay p.1.2 ((FiatShamirWithAbort ids hr M maxAttempts).verify + pk p.1.1.1 p.1.1.2))).run p.2] := by + -- STEP 1 (the link fusion, executed by the `simp only` below): collapse the explicit `.run ∅` + -- re-simulation boundary on the RHS. Distributing the outer `simulateQ` over + -- `simulatedNmaAdv`'s post-processing bind (`simulateQ_bind`/`StateT.run_bind`) exposes the + -- nested managed run + -- `(simulateQ (unifFwd+ro) ((simulateQ ((unifSim+roSim)+sigSim) (adv.main pk)).run ∅)).run ∅`, + -- which `managedRun_eq_link_run` rewrites to the canonical `linkReshape` of a *single* linked + -- simulation `(simulateQ nmaLinkImpl (adv.main pk)).run (∅, ∅)` over the product cache + -- `(inner managed cache, outer runtime cache)`. After this rewrite the RHS is a single + -- `simulateQ` whose state is genuinely the inner/outer cache pair. + -- + -- WHY THE COUPLING RUNS ON A LAYERED STATE. The two sides are not related by a state projection + -- out of the plain hybrid state `(reCache, signed)`. Replaying both handlers per step, the + -- linked caches evolve as: + -- * `outerCache` accumulates *only live RO reads* (`roSim` forwards inner misses to `fwd`, + -- re-simulated by the inner `randomOracle`, which writes the outer layer); signing's + -- `sigSim` programs the *inner* layer only and never forwards to the outer oracle; + -- * `innerCache` accumulates *both* live RO reads *and* the signing-programmed points. + -- So `outerCache = reCache ∖ {signing-only-programmed points}` is not a function of + -- `(reCache, signed)`: a point `(msg, w)` with `msg ∈ signed` may have entered `reCache` either + -- by a live RO read (then it is in `outerCache`) or by `signProgramCont` (then it is absent + -- from `outerCache`), and the plain hybrid state records no flag distinguishing the two. The + -- proof therefore runs the adversary on an *enriched, layered* cache state that tags each entry + -- as live-read (base layer) vs signing-programmed (ghost layer) — the same `overlayCache` / + -- ghost-layer device used for the Prog→Trans hop in `GhostBodies` — namely `ghostNmaImpl` over + -- `NmaGhostState = ((baseCache, ghostCache), signed)`. On that state the partition *is* a state + -- function and both projections are per-step state projections. + simp only [simulatedNmaAdv, simulateQ_bind, StateT.run_bind, bind_assoc] + -- The RHS is now `(fun x => x.1) <$> do let p ← (simulateQ (unifFwd+ro) + -- ((simulateQ ((unifSim+roSim)+sigSim) (adv.main pk)).run ∅)).run ∅; (Option-B post)…`, + -- with the bare nested managed run exposed. `managedRun_eq_link_run` equates this nested + -- run (modulo the canonical `linkReshape <$> _` regrouping of the final state) with the + -- single linked simulation `(simulateQ (outer.link inner) (adv.main pk)).run (∅, ∅)`. + -- + -- With the nested boundary exposed, the bound is the pair of state projections out of the + -- layered ghost-tagged run, followed by the verify-tail split. + -- + -- (a) HYBRID SIDE. `ghostNmaImpl` (`GhostBodies.lean`) runs the adversary over + -- `NmaGhostState = ((baseCache, ghostCache), signed)`, with + -- `simGhostSignBody`/`ghostSignProgramCont` writing the accepted transcript to the ghost layer + -- and the base oracles writing live RO reads to the base layer. Its overlay projection back to + -- the plain single-cache hybrid is `ghostNmaImpl_proj_hybrid` (per step) and + -- `map_run_simulateQ_ghostNmaImpl_overlay`/`_empty` (whole run), via + -- `OracleComp.map_run_simulateQ_eq_of_query_map_eq` with + -- `proj ((base, ghost), signed) = (overlayCache base ghost, signed)`. So the hybrid LHS equals + -- `Pr[= true | (overlay-projected ghostNmaImpl run) >>= …]`. + -- + -- (b) MANAGED SIDE. The *same* layered run projects onto the linked + -- `(outerCache : spec.QueryCache, innerCache : (M × Commit →ₒ Chal).QueryCache)` pair under + -- `proj2 ((base, ghost), signed) = (baseEmbed (overlayCache base ghost), base)`, + -- i.e. the inner managed cache is the full hybrid overlay (live reads *and* programmed sign + -- points) while the outer runtime cache is the live-read base layer only. Carrying the sign + -- point in the inner slot is what makes the sign step a state function whether or not the + -- programmed point coincides with a prior live read. This is not a primitive-query projection: + -- by `linkWith_apply_run` each `nmaLinkImpl t` step is itself a nested + -- `simulateQ nmaInnerImpl ((nmaOuterImpl t).run …)`, where `roSim` does an inner cache lookup + -- and forwards a miss to `fwd` (re-simulated by the inner `randomOracle`, the + -- `randomOracle_run_eq_roStep` round-trip) and `sigSim` runs a whole + -- `simulateQ unifSim (firstSome (sim pk) maxAttempts)`. Against that nested form every per-step + -- coupling is an exact unconditional equality — `hproj2_unif`, `hproj2_ro`, + -- `hproj2_ro_ghost_hit`, `hproj2_ro_fresh`, `hproj2_sign` — bundled as `hproj2_evalDist` and + -- lifted to the whole run by `evalDist_map_run_simulateQ_ghostNmaImpl_proj2` (via + -- `relTriple_simulateQ_run` and the graph coupling `relTriple_graph_of_evalDist_map_eq`). + -- The supporting facts are `ghostNmaImpl_preserves_signed_inv` (every ghost-layer point's msg + -- lies in `signed`, the NMA analogue of `ghostHybridImpl_preserves_signed_inv`, backed by + -- `simGhostSignBody_support_ghost`) and `baseEmbed` + -- (+ `baseEmbed_inr`/`baseEmbed_inl`/`baseEmbed_cacheQuery`), the embedding of the base RO + -- cache (keyed by `M × Commit`) into the sum-spec-keyed outer runtime cache, whose RO-step + -- algebra is `baseEmbed (base.cacheQuery mc v) = (baseEmbed base).cacheQuery (.inr mc) v`. + -- + -- (c) ASSEMBLY (the verify-tail split, executed below). By (a) and (b) the hybrid LHS run and + -- the linked managed RHS run are both projections of the *same* layered ghost-tagged run + -- `(simulateQ ghostNmaImpl (adv.main pk)).run ((∅,∅), [])`. The two verify tails are aligned on + -- this common run by `probOutput_hybridVerifyCont_le_managed_verify` — on `msg ∈ signed` the + -- freshness conjunct zeroes the hybrid side (`probOutput_true_hybridVerifyCont_of_mem`), and on + -- fresh forgeries the overlay verification agrees with the live verification + -- (`withCacheOverlay_verify_eq_of_miss`, `hybridVerifyCont_cache_congr`), gated by the whole-run + -- ghost-domain invariant `ghostNmaImpl_run_signed_inv`. The `linkReshape` / post-processing + -- regrouping is threaded by `managedRun_eq_link_run` + `bind_map_left`. + -- + -- Reduce the Option-B post-processing `pure` (re-simulated under `nmaInner`) to its value, and + -- pull the outer `(fun x => x.1) <$> _` past the head bind. The RHS is now `nestedManaged >>= K` + -- with `K a = (fun x => x.1) <$> (simulateQ nmaInner (withCacheOverlay (advCache a) + -- (verify pk a.1.1.1 a.1.1.2))).run a.2`. + simp only [simulateQ_pure, StateT.run_pure, pure_bind, map_bind] + -- The managed verify tail, expressed as a function of the *value × linked cache pair*. By + -- `proj2` it is the layered-run tail; by `linkReshape` it is the nested managed tail. + set RHSverify : (M × Option (Commit × Resp)) × + ((unifSpec + (M × Commit →ₒ Chal)).QueryCache × (M × Commit →ₒ Chal).QueryCache) → + ProbComp Bool := + fun p => (fun x : Bool × _ => x.1) <$> + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + + (randomOracle : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp))) + (withCacheOverlay + (match p.1.2 with + | some (w', _) => Function.update p.2.1 (Sum.inr (p.1.1, w')) none + | none => p.2.1) + ((FiatShamirWithAbort ids hr M maxAttempts).verify pk p.1.1 p.1.2))).run p.2.2 + with hRHSverify + -- LHS: rewrite the plain hybrid run as the overlay projection of the layered ghost run (a), + -- and push the projection through the bind (`map_bind`). + rw [← map_run_simulateQ_ghostNmaImpl_overlay_empty M maxAttempts sim pk sk (adv.main pk), + bind_map_left] + -- RHS: fold the unfolded handlers back to `nmaOuterImpl`/`nmaInnerImpl`, regroup the nested + -- managed run by `managedRun_eq_link_run` into `linkRun`, then transport `linkRun`'s + -- distribution back to the layered ghost run by sub-lemma (b). + have hRHS : + Pr[= true | (simulateQ (nmaInnerImpl M) + ((simulateQ (nmaOuterImpl M maxAttempts sim pk) (adv.main pk)).run ∅)).run ∅ >>= + fun a => RHSverify (a.1.1, (a.1.2, a.2))] = + Pr[= true | (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) (adv.main pk)).run + ((∅, ∅), []) >>= fun g => RHSverify (g.1, proj2 M g.2)] := by + -- The ghost-side tail factors through `Prod.map id proj2`; the nested-side tail factors + -- through `linkReshape`. Rewriting both tails as `RHSverify <$> (the projected head)` via + -- `bind_map_left` lets sub-lemma (b) (`proj2 <$> ghostRun =𝒟 linkRun`) and the fusion + -- (`linkRun = linkReshape <$> nested`) line the two heads up. + have hproj2_empty : proj2 M (((∅ : (M × Commit →ₒ Chal).QueryCache), + (∅ : (M × Commit →ₒ Chal).QueryCache)), ([] : List M)) = (∅, ∅) := by + simp only [proj2, overlayCache_empty] + exact congrArg (·, ∅) (baseEmbed_empty M) + have hghost : + (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) (adv.main pk)).run ((∅, ∅), []) >>= + (fun g => RHSverify (g.1, proj2 M g.2)) = + (Prod.map id (proj2 M) <$> + (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) (adv.main pk)).run + ((∅, ∅), [])) >>= RHSverify := by + rw [bind_map_left]; rfl + have hnested : + (simulateQ (nmaInnerImpl M) + ((simulateQ (nmaOuterImpl M maxAttempts sim pk) (adv.main pk)).run ∅)).run ∅ >>= + (fun a => RHSverify (a.1.1, (a.1.2, a.2))) = + ((QueryImpl.Stateful.Frame.prod (unifSpec + (M × Commit →ₒ Chal)).QueryCache + ((M × Commit →ₒ Chal).QueryCache)).linkReshape (∅, ∅) <$> + (simulateQ (nmaInnerImpl M) + ((simulateQ (nmaOuterImpl M maxAttempts sim pk) (adv.main pk)).run ∅)).run ∅) >>= + RHSverify := by + rw [bind_map_left]; rfl + rw [hghost, hnested] + -- Reduce to the head-distribution equality, then bind with `RHSverify`. + have hhead : + 𝒟[Prod.map id (proj2 M) <$> + (simulateQ (ghostNmaImpl M maxAttempts sim pk sk) (adv.main pk)).run ((∅, ∅), [])] = + 𝒟[(QueryImpl.Stateful.Frame.prod (unifSpec + (M × Commit →ₒ Chal)).QueryCache + ((M × Commit →ₒ Chal).QueryCache)).linkReshape (∅, ∅) <$> + (simulateQ (nmaInnerImpl M) + ((simulateQ (nmaOuterImpl M maxAttempts sim pk) (adv.main pk)).run ∅)).run ∅] := by + rw [evalDist_map_run_simulateQ_ghostNmaImpl_proj2 M maxAttempts sim + pk sk (adv.main pk) ((∅, ∅), []), hproj2_empty, + managedRun_eq_link_run ids hr M maxAttempts sim adv pk] + refine OracleComp.probOutput_congr rfl ?_ + rw [evalDist_bind, evalDist_bind, hhead] + -- Assemble: the goal RHS is `nestedManaged >>= K` (`= hRHS`'s LHS, defeq), so rewrite to the + -- common ghost run, then `probOutput_bind_mono` against the pointwise verify-tail split, gated + -- by the whole-run ghost-domain invariant. + refine le_trans ?_ (le_of_eq hRHS.symm) + refine probOutput_bind_mono fun a ha => ?_ + obtain ⟨av, ⟨base, ghost⟩, signed⟩ := a + exact probOutput_hybridVerifyCont_le_managed_verify ids hr M maxAttempts pk av base ghost signed + (fun q hq => ghostNmaImpl_run_signed_inv M maxAttempts sim pk sk (adv.main pk) _ ha q hq) + +omit [SampleableType Stmt] in +/-- **Per-key cache-overlay invariant** (core of the NMA bridge): at a fixed key pair the +simulated single-cache hybrid (with the freshness check) is bounded by the run-normal-form +of the managed-RO NMA experiment — the managed-cache run of `simulatedNmaAdv` followed by +overlay verification, all under the runtime's `randomOracle` layer. + +This is the genuine distributional content of `probOutput_hybridExp_sim_le_managedRoNmaExp`: +the inner managed cache threaded by `roSim`/`sigSim` together with the runtime's outer +`randomOracle` layer reproduces the single-cache hybrid run of `hybridExpAtKey`, and on +fresh forgeries the `withCacheOverlay` verification agrees with the live oracle at the +verification point (a cache invariant in the style of `fsAbortSignLoop_cache_invariant`: +every entry programmed by the signing simulation has its message recorded in the signed +list, so the freshness conjunct can only decrease the left-hand side). -/ +lemma hybridExp_sim_le_managedRun_perKey + (ro : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp)) + (hro : ro = randomOracle) (pk : Stmt) (sk : Wit) : + Pr[= true | hybridExpAtKey ids hr M maxAttempts adv + (simSignBody M maxAttempts sim pk sk) pk] ≤ + Pr[= true | (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + ro) + ((simulatedNmaAdv ids hr M maxAttempts sim adv).main pk >>= fun result => + withCacheOverlay result.2 + ((FiatShamirWithAbort ids hr M maxAttempts).verify + pk result.1.1 result.1.2))).run' ∅] := by + subst hro + -- Put the hybrid LHS into run-normal-form (`run` of the hybrid handler on `adv.main pk` + -- followed by the verify-and-freshness tail `hybridVerifyCont`). + rw [hybridExpAtKey_eq_run_bind] + -- Put the managed RHS into run-normal-form: `simulateQ_bind` distributes the outer RO + -- simulation over the managed run and the overlay verification, and `StateT.run'`/`run` + -- exposes the `(forgery, runtimeCache)` bind as a `ProbComp` bind whose final value is the + -- forgery's verification bit (`pure p.1`). + rw [simulateQ_bind, StateT.run'_eq, StateT.run_bind] + exact hybridSimRun_le_managedRun_verify ids hr M maxAttempts sim adv pk sk + +omit [SampleableType Stmt] in +/-- NMA bridge: the success probability of the simulated hybrid (averaged over key +generation, with the freshness check) is at most the success probability of +`simulatedNmaAdv` in the managed-RO NMA experiment. + +Distributional content: (i) the single-cache-layer hybrid run coincides with the +managed-cache run of `simulatedNmaAdv` followed by overlay verification +(`withCacheOverlay`), and (ii) by a cache invariant in the style of +`fsAbortSignLoop_cache_invariant`, every entry programmed by the signing simulation has +its message recorded in the signed list, so on fresh forgeries the overlay agrees with +the live oracle at the verification point and the freshness conjunct can only decrease +the left-hand side. A hash-query-bound transfer in the style of +`FiatShamir.simulatedNmaAdv_hashQueryBound` (the loop issues no live hash queries) +should accompany this lemma when the downstream consumer needs NMA query bounds. -/ +lemma probOutput_hybridExp_sim_le_managedRoNmaExp : + Pr[= true | do + let (pk, sk) ← hr.gen + hybridExpAtKey ids hr M maxAttempts adv (simSignBody M maxAttempts sim pk sk) pk] ≤ + Pr[= true | SignatureAlg.managedRoNmaExp (runtime M) + (simulatedNmaAdv ids hr M maxAttempts sim adv)] := by + classical + -- Abbreviation for the runtime random-oracle simulator. + set ro : QueryImpl (M × Commit →ₒ Chal) + (StateT ((M × Commit →ₒ Chal).QueryCache) ProbComp) := randomOracle with hro + -- Normal form of the managed-RO NMA experiment: the runtime's `withStateOracle` + -- semantics unfolds to a single `simulateQ … |>.run' ∅`, and the lifted key + -- generation pulls out as an ordinary `ProbComp` bind via `roSim.run'_liftM_bind`. + have hRHS : Pr[= true | SignatureAlg.managedRoNmaExp (runtime M) + (simulatedNmaAdv ids hr M maxAttempts sim adv)] = + Pr[= true | hr.gen >>= fun pksk => + (simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + ro) + ((simulatedNmaAdv ids hr M maxAttempts sim adv).main pksk.1 >>= fun result => + withCacheOverlay result.2 + ((FiatShamirWithAbort ids hr M maxAttempts).verify + pksk.1 result.1.1 result.1.2))).run' ∅] := by + unfold SignatureAlg.managedRoNmaExp + -- Expose the bundled `withStateOracle` semantics as a run-normal-form ProbComp. + change Pr[= true | 𝒟[(simulateQ (unifFwdImpl (M × Commit →ₒ Chal) + ro) + (do + let (pk, _) ← (FiatShamirWithAbort ids hr M maxAttempts).keygen + let result ← (simulatedNmaAdv ids hr M maxAttempts sim adv).main pk + withCacheOverlay result.2 + ((FiatShamirWithAbort ids hr M maxAttempts).verify + pk result.1.1 result.1.2))).run' ∅]] = _ + -- `keygen = monadLift hr.gen`; pull it out of the simulation. + rw [show (FiatShamirWithAbort ids hr M maxAttempts).keygen = + (liftM hr.gen : OracleComp (unifSpec + (M × Commit →ₒ Chal)) (Stmt × Wit)) from rfl] + rw [simulateQ_bind, roSim.run'_liftM_bind] + rfl + rw [hRHS] + -- Reduce to a per-key statement under the shared `hr.gen` prefix. + refine probOutput_bind_mono fun pksk _ => ?_ + -- Per-key core: the simulated hybrid (with the freshness check) is bounded by the + -- managed-cache run of `simulatedNmaAdv` followed by overlay verification. This is the + -- cache-overlay invariant: the inner managed cache `roSim` plus the runtime's outer + -- `randomOracle` layer reproduces the single-cache hybrid, and on fresh forgeries the + -- overlay agrees with the live oracle at the verification point. + obtain ⟨pk, sk⟩ := pksk + exact hybridExp_sim_le_managedRun_perKey ids hr M maxAttempts sim adv ro hro pk sk + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/ReadRecording.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/ReadRecording.lean new file mode 100644 index 000000000..3b428eaf5 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/ReadRecording.lean @@ -0,0 +1,272 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.CouplingEngine + +/-! +# EUF-CMA for Fiat-Shamir with aborts: ReadRecording + +The read-recording handler `deferredDrawReadImpl`: the deferred-draw run's +read-time bad flag is dominated by the final-state predicate "some recorded +read-commit is in the final drawn list". + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ### Read-recording handler: bad as a final-state predicate + +The deferred-draw run's bad flag is set at *read time*: a read fires when its target commitment is +in the drawn list **as it stands at that read**. Because the drawn list only grows +(`deferredDraw_run_drawn_prefix`), a read that hits the drawn-so-far list certainly hits the *final* +drawn list, so the bad event is dominated by the final-state predicate "some recorded read-commit +is in the final drawn list". The handler `deferredDrawReadImpl` records, in an extra `List Commit` +component, the commitment `mc.2` of every adversarial read; its read step is otherwise identical to +`deferredDrawImpl` (same answer via `roStep`, same drawn list, same bad flag). The reduction +`deferredDraw_bad_le_readRecord` is the pointwise coupling that reads the bad ordering off the run; +it converts the *read-time* bad flag into the *final-state* membership predicate +`∃ rc ∈ readlist, rc ∈ drawnlist`, which removes the read-time/final-state mismatch that obstructs a +direct expectation bound. The recorded read commits are **value-free** (answers come from the real +layer via `roStep`; the drawn *values* never feed the read points), which is what +`Security/TapeFactorization.lean` charges against the recorded draws. + +Why a bespoke handler rather than generic query instrumentation. The framework's generic +`QueryImpl` decorations (`withLogging`, `withTrace`/`withTraceBefore`, and cursor/path-style +wrappers) instrument *occurrences of queries in the computation being simulated*: they observe the +query's input, and optionally its answer, at each node of the original tree. That is enough for the +read half of the state here — the adversary's random-oracle reads *are* nodes of `adv.main pk`, so +their commitment components could be logged generically. It is not enough for the draw half. The +commitment draws being recorded are not occurrences in the adversary's tree at all: they are +introduced *inside the signing handler*, by `ghostSignDrawBody`, when the handler answers a signing +query, and their number is itself random (one per rejected attempt). No input-only instrumentation +of the adversary's queries can name them, so the drawn list has to be a component of the handler's +own state, written by the signing branch. Recording both lists in one handler state is also what +makes the pair `(readlist, drawnlist)` available at a single final state, which is the form the +first-moment charge consumes. -/ + +/-- State of the read-recording deferred-draw handler: the underlying `DeferredState` together with +the accumulated list of commitment components `mc.2` of every adversarial random-oracle read. The +extra list makes the read-hit event a *final-state* predicate (membership in the drawn list) rather +than a read-time flag. -/ +abbrev DeferredReadState (M Commit Chal : Type) : Type := + DeferredState M Commit Chal × List Commit + +/-- The read-recording deferred-draw handler. Identical to `deferredDrawImpl` on the underlying +`DeferredState`, additionally appending the read's commitment component `mc.2` to the recorded +read-commit list on every adversarial random-oracle read. Uniform and signing steps leave the +read-commit list untouched. -/ +noncomputable def deferredDrawReadImpl (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (DeferredReadState M Commit Chal) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + (fun cu => (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> + roStep M s.1.1.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc => (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 + +omit [SampleableType Stmt] in +/-- **Coupling invariant for the read-recording reduction.** The underlying deferred state matches, +and whenever the deferred bad flag is set there is a recorded read-commit already in the deferred +drawn list. The drawn list grows monotonically, so a read-time hit (recorded in the bad flag) is +witnessed by a recorded read-commit lying in the *current* (hence final) drawn list. -/ +def deferredReadInv + (s₁ : DeferredState M Commit Chal) (s₂ : DeferredReadState M Commit Chal) : Prop := + s₁.1 = s₂.1.1 ∧ (s₁.2 = true → ∃ rc ∈ s₂.2, rc ∈ s₂.1.1.2) + +omit [SampleableType Stmt] in +/-- **Per-query coupling step for the read-recording reduction.** From any pair of +`deferredReadInv`-related states one step of `deferredDrawImpl` couples with one step of +`deferredDrawReadImpl` with equal output and the invariant preserved. + +* **Uniform** forwards the same draw; states untouched. +* **Read** answers from the shared real layer via `roStep` (same answer, same cache, same drawn + list); the recorded read-commit `mc.2` witnesses any newly-set bad flag (it is appended to the + read-commit list and, when the flag fires, lies in the drawn list). +* **Sign** runs the shared `ghostSignDrawBody`; the drawn list grows in lockstep, so an existing + read-commit witness is preserved (the drawn list only appends). -/ +theorem deferredDrawRead_step (pk : Stmt) (sk : Wit) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (u₁ : DeferredState M Commit Chal) (u₂ : DeferredReadState M Commit Chal) + (hu : deferredReadInv M u₁ u₂) : + OracleComp.ProgramLogic.Relational.RelTriple + ((deferredDrawImpl ids M maxAttempts pk sk t).run u₁) + ((deferredDrawReadImpl ids M maxAttempts pk sk t).run u₂) + (fun p₁ p₂ => p₁.1 = p₂.1 ∧ deferredReadInv M p₁.2 p₂.2) := by + obtain ⟨hst, hbad⟩ := hu + rcases t with (n | mc) | msg + · -- UNIFORM: both forward the same draw; state untouched. + have hrun₁ : (deferredDrawImpl ids M maxAttempts pk sk (.inl (.inl n))).run u₁ = + (fun u => (u, u₁)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + have hrun₂ : (deferredDrawReadImpl ids M maxAttempts pk sk (.inl (.inl n))).run u₂ = + (fun u => (u, u₂)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + rw [hrun₁, hrun₂] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + exact ⟨rfl, hst, hbad⟩ + · -- READ: shared `roStep`; the recorded read-commit witnesses any newly-set bad flag. + have hrun₁ : (deferredDrawImpl ids M maxAttempts pk sk (.inl (.inr mc))).run u₁ = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((cu.2, u₁.1.1.2), u₁.1.2), u₁.2 || decide (mc.2 ∈ u₁.1.2)))) <$> + roStep M u₁.1.1.1 mc := rfl + have hrun₂ : (deferredDrawReadImpl ids M maxAttempts pk sk (.inl (.inr mc))).run u₂ = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, u₂.1.1.1.2), u₂.1.1.2), u₂.1.2 || decide (mc.2 ∈ u₂.1.1.2)), + mc.2 :: u₂.2))) <$> + roStep M u₂.1.1.1.1 mc := rfl + rw [hrun₁, hrun₂, show u₁.1.1.1 = u₂.1.1.1.1 from by rw [hst], + show u₁.1.1.2 = u₂.1.1.1.2 from by rw [hst], show u₁.1.2 = u₂.1.1.2 from by rw [hst]] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + refine ⟨rfl, rfl, ?_⟩ + intro hb + rcases Bool.or_eq_true _ _ |>.mp hb with hb' | hb' + · obtain ⟨rc, hrcmem, hrcdraw⟩ := hbad hb' + exact ⟨rc, List.mem_cons_of_mem _ hrcmem, hrcdraw⟩ + · exact ⟨mc.2, List.mem_cons_self, by simpa using hb'⟩ + · -- SIGN: shared signing body; drawn list grows, witness preserved. + have hrun₁ : (deferredDrawImpl ids M maxAttempts pk sk (.inr msg)).run u₁ = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((alc.2, msg :: u₁.1.1.2), u₁.1.2 ++ alc.1.2), u₁.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run u₁.1.1.1 := rfl + have hrun₂ : (deferredDrawReadImpl ids M maxAttempts pk sk (.inr msg)).run u₂ = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: u₂.1.1.1.2), u₂.1.1.2 ++ alc.1.2), u₂.1.2), u₂.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run u₂.1.1.1.1 := rfl + rw [hrun₁, hrun₂, show u₁.1.1.1 = u₂.1.1.1.1 from by rw [hst], + show u₁.1.1.2 = u₂.1.1.1.2 from by rw [hst], show u₁.1.2 = u₂.1.1.2 from by rw [hst]] + refine OracleComp.ProgramLogic.Relational.relTriple_map (R := _) + (OracleComp.ProgramLogic.Relational.relTriple_post_mono + (OracleComp.ProgramLogic.Relational.relTriple_refl _) ?_) + rintro a b (rfl : a = b) + refine ⟨rfl, rfl, ?_⟩ + intro hb + obtain ⟨rc, hrcmem, hrcdraw⟩ := hbad hb + exact ⟨rc, hrcmem, List.mem_append_left _ hrcdraw⟩ + +omit [SampleableType Stmt] in +/-- **The read-recording run coupling.** By induction on the adversary computation `oa`, the +deferred-draw run and the read-recording run are coupled with `deferredReadInv` preserved at every +leaf, using `deferredDrawRead_step` per query and the inductive hypothesis for the continuation. +Mirrors `deferredCouple_run`. -/ +theorem deferredDrawRead_run {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s₁ : DeferredState M Commit Chal) (s₂ : DeferredReadState M Commit Chal), + deferredReadInv M s₁ s₂ → + OracleComp.ProgramLogic.Relational.RelTriple + ((simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s₁) + ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s₂) + (fun q₁ q₂ => deferredReadInv M q₁.2 q₂.2) := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s₁ s₂ hinv + simp only [simulateQ_pure, StateT.run_pure] + exact OracleComp.ProgramLogic.Relational.relTriple_pure_pure hinv + | query_bind t ob ih => + intro s₁ s₂ hinv + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + refine OracleComp.ProgramLogic.Relational.relTriple_bind + (deferredDrawRead_step ids M maxAttempts pk sk t s₁ s₂ hinv) ?_ + rintro p₁ p₂ ⟨hout, hinv'⟩ + rw [show p₁.1 = p₂.1 from hout] + exact ih p₂.1 p₁.2 p₂.2 hinv' + +omit [SampleableType Stmt] in +/-- **The read-recording reduction.** The deferred-draw run's bad marginal is at most the +read-recording run's final-state predicate "some recorded read-commit lies in the final drawn +list", from any pair of `deferredReadInv`-related start states. + +Reads off the bad-ordering component of `deferredReadInv` from the run coupling +`deferredDrawRead_run` via `probEvent_le_of_relTriple_imp`. This converts the read-time bad flag of +`deferredDrawImpl` into the membership predicate over the read-recording run's final state, where +both the recorded read-commit list and the drawn list are available together. -/ +theorem deferredDraw_bad_le_readRecord {γ : Type} + (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (s₁ : DeferredState M Commit Chal) (s₂ : DeferredReadState M Commit Chal) + (hinv : deferredReadInv M s₁ s₂) : + Pr[fun z : γ × DeferredState M Commit Chal => z.2.2 = true | + (simulateQ (deferredDrawImpl ids M maxAttempts pk sk) oa).run s₁] + ≤ Pr[fun z : γ × DeferredReadState M Commit Chal => ∃ rc ∈ z.2.2, rc ∈ z.2.1.1.2 | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s₂] := + OracleComp.ProgramLogic.Relational.probEvent_le_of_relTriple_imp + (deferredDrawRead_run ids M maxAttempts pk sk oa s₁ s₂ hinv) + (fun _ _ hp hbad => hp.2 hbad) + +omit [SampleableType Stmt] in +/-- **Markov reduction for the read-recording firing event.** The read-recording run's final-state +read-hit predicate `∃ rc ∈ readlist, rc ∈ drawnlist` has probability at most the *expected +coincidence count* `E[#{ rc ∈ readlist : rc ∈ drawnlist }]`, the first moment of the number of +recorded read-commits that lie in the drawn list. + +This is the elementary first-moment (Markov) step of the per-position route: a firing run has at +least one coincidence, so the indicator of the firing event is dominated by the (nonnegative, +integer-valued) coincidence count, and `Pr[fire] ≤ E[count]` by the Markov core +`probEvent_le_tsum_probOutput_mul_cost`. The probabilistic content — bounding `E[count]` by +`(qH+1)·ε·E[#attempts]` — is the per-position independence of each fresh draw from the +value-free recorded read-commit list, supplied by `readRecord_expected_coincidences_le`. This +lemma is exactly the step that isolates that independence from the firing event. -/ +theorem readRecord_pred_le_expected_coincidences {γ : Type} + (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (s : DeferredReadState M Commit Chal) : + Pr[fun z : γ × DeferredReadState M Commit Chal => ∃ rc ∈ z.2.2, rc ∈ z.2.1.1.2 | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] + ≤ ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + (z.2.2.countP (fun rc => decide (rc ∈ z.2.1.1.2)) : ℝ≥0∞) := by + classical + refine probEvent_le_tsum_probOutput_mul_cost _ _ _ (fun z hz => ?_) + obtain ⟨rc, hrc, hrd⟩ := hz + have hpos : 0 < z.2.2.countP (fun rc => decide (rc ∈ z.2.1.1.2)) := by + rw [List.countP_pos_iff] + exact ⟨rc, hrc, by simpa using hrd⟩ + have h1 : (1 : ℕ) ≤ z.2.2.countP (fun rc => decide (rc ∈ z.2.1.1.2)) := hpos + exact_mod_cast h1 + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort diff --git a/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/TapeFactorization.lean b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/TapeFactorization.lean new file mode 100644 index 000000000..f2456aaa7 --- /dev/null +++ b/VCVio/CryptoFoundations/FiatShamir/WithAbort/Security/TapeFactorization.lean @@ -0,0 +1,2570 @@ +/- +Copyright (c) 2026 Quang Dao. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao +-/ + +module + +public import VCVio.CryptoFoundations.FiatShamir.WithAbort.Security.ReadRecording + +/-! +# EUF-CMA for Fiat-Shamir with aborts: TapeFactorization + +The first-moment reduction scaffolding for the coincidence-count bound and +the fold-level tape factorization: every interleaved signing query's attempt-draw +block commutes to the front of the opaque adversary fold, so the run distributes as +a pre-drawn tape followed by a tape-consuming run. + +Part of the CMA-to-NMA security development for the Fiat-Shamir-with-aborts +transform; `VCVio.CryptoFoundations.FiatShamir.WithAbort.Security` re-exports +all of its modules and holds the overview docstring. +-/ + +@[expose] public section + +universe u v + +open OracleComp OracleSpec +open scoped BigOperators ENNReal + +variable {Stmt Wit Commit PrvState Chal Resp : Type} {rel : Stmt → Wit → Bool} + +namespace FiatShamirWithAbort + +section EUF_CMA + +variable [SampleableType Stmt] +variable [DecidableEq Commit] [SampleableType Chal] +variable (ids : IdenSchemeWithAbort Stmt Wit Commit PrvState Chal Resp rel) + (hr : GenerableRelation Stmt Wit rel) + (M : Type) [DecidableEq M] (maxAttempts : ℕ) + +section scaffold + +variable (sim : Stmt → ProbComp (Option (Commit × Chal × Resp))) +variable (adv : SignatureAlg.unforgeableAdv + (FiatShamirWithAbort + (m := OracleComp (unifSpec + (M × Commit →ₒ Chal))) ids hr M maxAttempts)) + +/-! ### First-moment reduction scaffolding for the coincidence-count bound + +The numeric residual `readRecord_expected_coincidences_le` reduces, by elementary arithmetic, to a +single value-free cross-term atom. The reduction chain: + +* the coincidence count is dominated by the pair count + `Σ_{rc ∈ readlist} drawnlist.count rc` (`List.countP_le_sum_count_mem`); +* the *number of recorded reads* is bounded **deterministically** by the read-query budget `qH` + (`deferredDrawReadImpl_run_readlist_length_le`), so the read-recording run's readlist length is at + most `s.readlist.length + qH`; +* the expected drawn-list length of the read-recording run is at most + `s.drawnlist.length + qSrem · (1/(1-p))` (`deferredDrawRead_run_expected_drawnlist_length_le`, the + read-recording counterpart of `deferredDraw_run_expected_length_le`); +* the genuine content is then the **value-free per-pair atom** + `readRecord_expected_pairs_nontape_le`: the expected pair count is at most `ε` times the expected + `readlist.length · drawnlist.length`, because each recorded drawn commit is a fresh raw + `Prod.fst <$> ids.commit` draw (mass `≤ ε`) and is independent of the value-free recorded + read-commit list. + +`readRecord_expected_coincidences_le` chains these with the deterministic read bound +(`readlist.length ≤ qH+1` from the empty start) and the final-arithmetic conversion. + +The tape factorization (`tapeDrawReadImpl`, `evalDist_deferredDrawRead_eq_drawList_tapeDrawRead`, +`readRecord_expected_pairs_tape_le`, `readRecord_expected_pairs_le`) is a separate, reusable +front-loading representation of the same run; it is not on the live path of the chain above. -/ + +/-- Domination of the membership count by the per-element coincidence count: the number of recorded +read-commits lying in the drawn list is at most `Σ_{rc ∈ readlist} drawnlist.count rc`, the total +number of coinciding `(read, draw)` pairs. -/ +private lemma countP_mem_le_sum_count {α : Type} [DecidableEq α] (l d : List α) : + l.countP (fun rc => decide (rc ∈ d)) ≤ (l.map (fun rc => d.count rc)).sum := by + induction l with + | nil => simp + | cons a t ih => + rw [List.countP_cons, List.map_cons, List.sum_cons] + by_cases h : a ∈ d + · simp only [decide_eq_true_eq, h, if_true] + have : 1 ≤ d.count a := List.one_le_count_iff.mpr h + omega + · simp only [decide_eq_true_eq, h, if_false] + omega + +/-- Expressing a `List.count` as a sum of equality indicators over the list. -/ +private lemma count_eq_sum_map_ite {α : Type} [DecidableEq α] (d : List α) (a : α) : + (d.map (fun w => (if w = a then 1 else 0))).sum = d.count a := by + induction d with + | nil => simp + | cons x d ih => + simp only [List.map_cons, List.sum_cons, ih, List.count_cons] + by_cases h : x = a + · simp [h]; ring + · simp [h] + +/-- **Symmetric double-count of two lists.** Summing `d.count rc` over `rc ∈ l` equals summing +`l.count w` over `w ∈ d`; both count the coinciding `(read, draw)` pairs +(`Σ_x l.count x · d.count x`). This re-index lets the per-pair charge be organised by the +*draw* list (whose entries are fresh i.i.d. commitments) rather than the read list. -/ +private lemma sum_map_count_comm {α : Type} [DecidableEq α] (l d : List α) : + (l.map (fun rc => d.count rc)).sum = (d.map (fun w => l.count w)).sum := by + induction l with + | nil => simp + | cons a l ih => + simp only [List.map_cons, List.sum_cons, ih] + have key : (d.map (fun w => (a :: l).count w)).sum + = (d.map (fun w => (if w = a then 1 else 0))).sum + (d.map (fun w => l.count w)).sum := by + rw [← List.sum_map_add] + refine congrArg _ (List.map_congr_left fun w _ => ?_) + rw [List.count_cons] + by_cases h : w = a + · subst h; simp [add_comm] + · simp [h, Ne.symm h] + rw [key, count_eq_sum_map_ite, add_comm] + +omit [SampleableType Stmt] in +/-- **Deterministic readlist-length bound.** Every reachable final state of the read-recording run +records at most `qH` new read-commits, where `qH` bounds the random-oracle (read) queries `oa` makes +(the `(· matches .inl (.inr _))` component of `signHashQueryBound`): each read step prepends exactly +one commitment to the recorded read-commit list and uniform/signing steps leave it untouched. Hence +`readlist.length ≤ s.readlist.length + qH` on the whole support — a *deterministic* (support-wide) +bound, used to dominate the random `readlist.length` factor of the pair count by the constant +`qH`. -/ +theorem deferredDrawReadImpl_run_readlist_length_le {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qH : ℕ), oa.IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH → + ∀ (s : DeferredReadState M Commit Chal) + (z : γ × DeferredReadState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s) → + z.2.2.length ≤ s.2.length + qH := by + induction oa using OracleComp.inductionOn with + | pure a => + intro qH _ s z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz; simp + | query_bind t ob ih => + intro qH hQ s z hz + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rw [simulateQ_query_bind, StateT.run_bind, mem_support_bind_iff] at hz + obtain ⟨x, hx, hzx⟩ := hz + rcases t with (n | mc) | msg + · -- UNIFORM: readlist untouched; budget unchanged. + have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs + have hih := ih u qH (by simpa using hQ2 u) s z hzx + simpa using hih + · -- READ: readlist grows by one; budget decrements by one (`0 < qH`). + have hpos : 0 < qH := by + rcases hQ1 with hno | hpos + · exact absurd rfl hno + · exact hpos + have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> + roStep M s.1.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs + have hih := ih cu.1 (qH - 1) (by simpa using hQ2 cu.1) + ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), mc.2 :: s.2) z hzx + simp only [List.length_cons] at hih + omega + · -- SIGN: readlist untouched; budget unchanged. + have hxs : x ∈ support ((fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1) := hx + rw [support_map] at hxs + obtain ⟨alc, _, rfl⟩ := hxs + have hih := ih alc.1.1 qH (by simpa using hQ2 alc.1.1) + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2) z hzx + simpa using hih + +omit [SampleableType Stmt] in +/-- **Per-step expected drawn-list length growth of the read-recording handler.** One step of +`deferredDrawReadImpl` grows the expected drawn-list length by at most `1/(1-p)` on a signing query +and by `0` on uniform / random-oracle-read queries (which leave the drawn list `s.1.1.2` +untouched). Identical to `deferredDrawImpl_step_expected_length_le` on the underlying deferred +state; the extra read-commit list is irrelevant to the drawn-list length. -/ +lemma deferredDrawReadImpl_step_expected_drawnlist_length_le (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : DeferredReadState M Commit Chal) : + (∑' z : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) × + DeferredReadState M Commit Chal, + Pr[= z | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (z.2.1.1.2.length : ℝ≥0∞)) + ≤ (s.1.1.2.length : ℝ≥0∞) + + (if (t matches Sum.inr _) then ENNReal.ofReal (1 / (1 - p_abort)) else 0) := by + classical + rcases t with (n | mc) | msg + · -- UNIFORM: state untouched, drawn list `s.1.1.2` preserved. + rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ (by simp [deferredDrawReadImpl])) + intro z hz + have hzs : z ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hz + rw [support_map] at hzs + obtain ⟨u, _, rfl⟩ := hzs; rfl + · -- READ: writes only the base cache / bad flag / readlist; drawn list `s.1.1.2` preserved. + rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ ?_) + · intro z hz + have hzs : z ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> + roStep M s.1.1.1.1 mc) := hz + rw [support_map] at hzs + obtain ⟨cu, _, rfl⟩ := hzs; rfl + · simp only [deferredDrawReadImpl, StateT.run_mk] + rcases hg : s.1.1.1.1 mc with _ | v <;> simp [roStep, hg] + · -- SIGN: drawn list becomes `s.1.1.2 ++ alc.1.2`; expected new length ≤ 1/(1-p). + rw [if_pos (by simp)] + have hrun : (deferredDrawReadImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 := rfl + rw [hrun] + refine le_of_eq_of_le (tsum_probOutput_map_mul + ((ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1) + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) + (fun z => (z.2.1.1.2.length : ℝ≥0∞))) ?_ + calc _ + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ((s.1.1.2.length : ℝ≥0∞) + (alc.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun alc => ?_ + simp only [List.length_append] + push_cast + ring + _ = (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (s.1.1.2.length : ℝ≥0∞)) + + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (alc.1.2.length : ℝ≥0∞) := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun alc => by rw [mul_add] + _ ≤ (s.1.1.2.length : ℝ≥0∞) + ENNReal.ofReal (1 / (1 - p_abort)) := by + refine add_le_add ?_ ?_ + · rw [ENNReal.tsum_mul_right, tsum_probOutput_eq_one' (by simp), one_mul] + · exact le_trans (tsum_probOutput_run_ghostSignDrawBody_mul_length_le ids M pk sk msg + hAbort maxAttempts s.1.1.1.1) (geomAttemptSum_le maxAttempts hp₀ hp) + +omit [SampleableType Stmt] in +/-- **Run-level expected drawn-list length of the read-recording run.** By induction on `oa`, the +expected final drawn-list length of the read-recording run from a start state `s` is at most +`s.drawnlist.length + qSrem · (1/(1-p))`, where `qSrem` bounds the number of signing queries. The +read-recording counterpart of `deferredDraw_run_expected_length_le`: the drawn list evolves +identically (the recorded read-commit list never affects it), so the per-step charge +`deferredDrawReadImpl_step_expected_drawnlist_length_le` telescopes against the signing-query budget +exactly as before. -/ +theorem deferredDrawRead_run_expected_drawnlist_length_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qSrem : ℕ), oa.IsQueryBoundP (· matches Sum.inr _) qSrem → + ∀ (s : DeferredReadState M Commit Chal), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + (z.2.1.1.2.length : ℝ≥0∞)) + ≤ (s.1.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + induction oa using OracleComp.inductionOn with + | pure a => + intro qSrem _ s + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + exact le_self_add + | query_bind t ob ih => + intro qSrem hQ s + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rw [simulateQ_query_bind, StateT.run_bind, tsum_probOutput_bind_mul] + set c : ℝ≥0∞ := ENNReal.ofReal (1 / (1 - p_abort)) with hc + have hmass : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredReadState M Commit Chal, + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s]) = 1 := + tsum_probOutput_eq_one' (by + rcases t with (n | mc) | msg + · simp [deferredDrawReadImpl] + · simp only [deferredDrawReadImpl, StateT.run_mk] + rcases hg : s.1.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawReadImpl]) + have hfold : ∀ (b : ℕ) (extra : ℝ≥0∞), + (∀ x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredReadState M Commit Chal, + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * (z.2.1.1.2.length : ℝ≥0∞)) + ≤ (x.2.1.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c) → + (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.1.2.length : ℝ≥0∞)) ≤ (s.1.1.2.length : ℝ≥0∞) + extra → + extra + (b : ℝ≥0∞) * c ≤ (qSrem : ℝ≥0∞) * c → + (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * (z.2.1.1.2.length : ℝ≥0∞)) + ≤ (s.1.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + intro b extra hcont hstep hbudget + calc (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * (z.2.1.1.2.length : ℝ≥0∞)) + ≤ ∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c) := + ENNReal.tsum_le_tsum fun x => by gcongr; exact hcont x + _ = (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.1.2.length : ℝ≥0∞)) + (b : ℝ≥0∞) * c := by + rw [show (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.1.2.length : ℝ≥0∞) + (b : ℝ≥0∞) * c)) + = ∑' x, (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (x.2.1.1.2.length : ℝ≥0∞) + + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((b : ℝ≥0∞) * c)) from tsum_congr fun x => by rw [mul_add]] + rw [ENNReal.tsum_add, ENNReal.tsum_mul_right, hmass, one_mul] + _ ≤ ((s.1.1.2.length : ℝ≥0∞) + extra) + (b : ℝ≥0∞) * c := by gcongr + _ ≤ (s.1.1.2.length : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by rw [add_assoc]; gcongr + rcases t with (n | mc) | msg + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawReadImpl_step_expected_drawnlist_length_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inl n)) s + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawReadImpl_step_expected_drawnlist_length_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inr mc)) s + · have hpos : 0 < qSrem := by + rcases hQ1 with hno | hpos + · exact absurd (by simp) hno + · exact hpos + refine hfold (qSrem - 1) c (fun x => ih x.1 (qSrem - 1) (by simpa using hQ2 x.1) x.2) ?_ ?_ + · have hstep := deferredDrawReadImpl_step_expected_drawnlist_length_le ids M maxAttempts + pk sk hp₀ hp hAbort (.inr msg) s + rwa [if_pos (by rfl), ← hc] at hstep + · rw [add_comm, ← add_one_mul, + show ((qSrem - 1 : ℕ) : ℝ≥0∞) + 1 = (qSrem : ℝ≥0∞) by + have : qSrem - 1 + 1 = qSrem := by omega + rw [← this]; push_cast; ring] + +omit [SampleableType Stmt] in +/-- **Per-step expected attempt-count growth of the read-recording handler.** One step of +`deferredDrawReadImpl` grows the expected combined size `drawnlist.length + signedlist.length` by at +most `1/(1-p)` on a signing query and by `0` on a uniform or random-oracle-read query (which leave +both lists untouched). The read-recording counterpart of +`deferredDrawImpl_step_expected_attemptCount_le`; the recorded read-commit list never affects the +drawn or signed lists, so the charge is identical. -/ +lemma deferredDrawReadImpl_step_expected_attemptCount_le (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (t : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) + (s : DeferredReadState M Commit Chal) : + (∑' z : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range t) × + DeferredReadState M Commit Chal, + Pr[= z | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + + (if (t matches Sum.inr _) then ENNReal.ofReal (1 / (1 - p_abort)) else 0) := by + classical + rcases t with (n | mc) | msg + · rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ (by simp [deferredDrawReadImpl])) + intro z hz + have hzs : z ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hz + rw [support_map] at hzs + obtain ⟨u, _, rfl⟩ := hzs; rfl + · rw [if_neg (by simp), add_zero] + refine le_of_eq (tsum_probOutput_mul_of_const_on_support _ ?_ ?_) + · intro z hz + have hzs : z ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> + roStep M s.1.1.1.1 mc) := hz + rw [support_map] at hzs + obtain ⟨cu, _, rfl⟩ := hzs; rfl + · simp only [deferredDrawReadImpl, StateT.run_mk] + rcases hg : s.1.1.1.1 mc with _ | v <;> simp [roStep, hg] + · rw [if_pos (by simp)] + have hrun : (deferredDrawReadImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 := rfl + rw [hrun] + refine le_of_eq_of_le (tsum_probOutput_map_mul + ((ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1) + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) + (fun z => ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞))) ?_ + calc _ + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + (1 : ℝ≥0∞) + + (alc.1.2.length : ℝ≥0∞)) := by + refine tsum_congr fun alc => ?_ + simp only [List.length_append, List.length_cons] + push_cast + ring + _ = ((∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + (1 : ℝ≥0∞))) + + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (alc.1.2.length : ℝ≥0∞)) := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun alc => by rw [mul_add] + _ ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + + ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [ENNReal.tsum_mul_right, tsum_probOutput_eq_one' (by simp), one_mul] + rw [add_assoc] + gcongr + refine le_trans (add_le_add_right + (tsum_probOutput_run_ghostSignDrawBody_mul_length_le_tight ids M pk sk msg + hAbort maxAttempts s.1.1.1.1) _) ?_ + rw [add_comm] + refine le_trans (le_of_eq ?_) (geomSum_le hp₀ hp (maxAttempts + 1)) + rw [Finset.sum_range_succ'] + simp only [pow_zero] + +omit [SampleableType Stmt] in +/-- **Run-level expected attempt count of the read-recording run.** By induction on `oa`, the +expected combined size `drawnlist.length + signedlist.length` of the read-recording run from a start +state `s` is at most `(s.drawnlist.length + s.signedlist.length) + qSrem · (1/(1-p))`, where `qSrem` +bounds the number of signing queries. The read-recording counterpart of +`deferredDraw_run_expected_attemptCount_le`. Subtracting the start signed-list length `l.length` +gives the attempt-count mean `≤ qSrem/(1-p)` used by the sound `#attempts`-form coincidence +bound. -/ +theorem deferredDrawRead_run_expected_attemptCount_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qSrem : ℕ), oa.IsQueryBoundP (· matches Sum.inr _) qSrem → + ∀ (s : DeferredReadState M Commit Chal), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + induction oa using OracleComp.inductionOn with + | pure a => + intro qSrem _ s + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + exact le_self_add + | query_bind t ob ih => + intro qSrem hQ s + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rw [simulateQ_query_bind, StateT.run_bind, tsum_probOutput_bind_mul] + set c : ℝ≥0∞ := ENNReal.ofReal (1 / (1 - p_abort)) with hc + have hmass : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredReadState M Commit Chal, + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s]) = 1 := + tsum_probOutput_eq_one' (by + rcases t with (n | mc) | msg + · simp [deferredDrawReadImpl] + · simp only [deferredDrawReadImpl, StateT.run_mk] + rcases hg : s.1.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawReadImpl]) + have hfold : ∀ (b : ℕ) (extra : ℝ≥0∞), + (∀ x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range t) × DeferredReadState M Commit Chal, + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c) → + (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + extra → + extra + (b : ℝ≥0∞) * c ≤ (qSrem : ℝ≥0∞) * c → + (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + intro b extra hcont hstep hbudget + calc (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob x.1)).run x.2] * ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c) := + ENNReal.tsum_le_tsum fun x => by gcongr; exact hcont x + _ = (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + (b : ℝ≥0∞) * c := by + rw [show (∑' x, Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + (((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞) + (b : ℝ≥0∞) * c)) + = ∑' x, (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((x.2.1.1.2.length + x.2.1.1.1.2.length : ℕ) : ℝ≥0∞) + + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk t).run s] * + ((b : ℝ≥0∞) * c)) from tsum_congr fun x => by rw [mul_add]] + rw [ENNReal.tsum_add, ENNReal.tsum_mul_right, hmass, one_mul] + _ ≤ (((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + extra) + (b : ℝ≥0∞) * c := by + gcongr + _ ≤ ((s.1.1.2.length + s.1.1.1.2.length : ℕ) : ℝ≥0∞) + (qSrem : ℝ≥0∞) * c := by + rw [add_assoc]; gcongr + rcases t with (n | mc) | msg + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawReadImpl_step_expected_attemptCount_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inl n)) s + · refine hfold qSrem 0 (fun x => ih x.1 qSrem (by simpa using hQ2 x.1) x.2) ?_ (by simp) + simpa using deferredDrawReadImpl_step_expected_attemptCount_le ids M maxAttempts pk sk + hp₀ hp hAbort (.inl (.inr mc)) s + · have hpos : 0 < qSrem := by + rcases hQ1 with hno | hpos + · exact absurd (by simp) hno + · exact hpos + refine hfold (qSrem - 1) c (fun x => ih x.1 (qSrem - 1) (by simpa using hQ2 x.1) x.2) ?_ ?_ + · have hstep := deferredDrawReadImpl_step_expected_attemptCount_le ids M maxAttempts + pk sk hp₀ hp hAbort (.inr msg) s + rwa [if_pos (by rfl), ← hc] at hstep + · rw [add_comm, ← add_one_mul, + show ((qSrem - 1 : ℕ) : ℝ≥0∞) + 1 = (qSrem : ℝ≥0∞) by + have : qSrem - 1 + 1 = qSrem := by omega + rw [← this]; push_cast; ring] + +omit [SampleableType Stmt] in +/-- **The read-recording run never fails.** Every step of `deferredDrawReadImpl` pushes forward a +non-failing `ProbComp`, so the whole fold has zero failure mass. -/ +theorem deferredDrawRead_run_neverFail {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredReadState M Commit Chal), + Pr[⊥ | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] = 0 := by + induction oa using OracleComp.inductionOn with + | pure a => intro s; simp [simulateQ_pure, StateT.run_pure] + | query_bind t ob ih => + intro s + rw [simulateQ_query_bind, StateT.run_bind, probFailure_bind_eq_zero_iff] + refine ⟨?_, fun x _ => ih x.1 x.2⟩ + rcases t with (n | mc) | msg + · simp [deferredDrawReadImpl] + · simp only [deferredDrawReadImpl] + rcases hg : s.1.1.1.1 mc with _ | v <;> simp [roStep, hg] + · simp [deferredDrawReadImpl] + +omit [SampleableType Stmt] in +/-- **The signed-message list of the read-recording run grows.** From any start state the recorded +signed-message list only ever gets longer, so its start length `l.length` is a lower bound on every +reachable final length. -/ +theorem deferredDrawRead_run_signed_prefix {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredReadState M Commit Chal) + (z : γ × DeferredReadState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s) → + s.1.1.1.2.length ≤ z.2.1.1.1.2.length := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz; exact le_rfl + | query_bind t ob ih => + intro s z hz + rw [simulateQ_query_bind, StateT.run_bind, mem_support_bind_iff] at hz + obtain ⟨x, hx, hzx⟩ := hz + refine le_trans ?_ (ih x.1 x.2 z hzx) + rcases t with (n | mc) | msg + · have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1) := hx + rw [support_map] at hxs + obtain ⟨alc, _, rfl⟩ := hxs; simp + +omit [SampleableType Stmt] in +/-- **The drawn-list of the read-recording run grows.** From any start state the recorded drawn +(rejected-commit) list only ever gets longer: uniform and read steps leave it untouched, signing +steps append the body's rejected draws. So the start length is a lower bound on every reachable +final length. -/ +theorem deferredDrawRead_run_drawn_prefix {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (s : DeferredReadState M Commit Chal) + (z : γ × DeferredReadState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s) → + s.1.1.2.length ≤ z.2.1.1.2.length := by + induction oa using OracleComp.inductionOn with + | pure a => + intro s z hz + simp only [simulateQ_pure, StateT.run_pure, support_pure, Set.mem_singleton_iff] at hz + subst hz; exact le_rfl + | query_bind t ob ih => + intro s z hz + rw [simulateQ_query_bind, StateT.run_bind, mem_support_bind_iff] at hz + obtain ⟨x, hx, hzx⟩ := hz + refine le_trans ?_ (ih x.1 x.2 z hzx) + rcases t with (n | mc) | msg + · have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs; exact le_rfl + · have hxs : x ∈ support ((fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1) := hx + rw [support_map] at hxs + obtain ⟨alc, _, rfl⟩ := hxs; simp + +omit [SampleableType Stmt] in +/-- **Read-recording attempt-count mean.** The constructed attempt count +`(drawnlist.length) + (signedlist.length - l.length)` (= #rejects + #signing-queries, the count that +soundly dominates the consumed-attempt positions) of the read-recording run from the empty-draw +start `((((re, l), []), false), [])` has mean at most `qSrem/(1-p)`. Mirrors +`deferredDraw_attemptKn_mean_le`: recover the total combined size by adding back `l.length`, valid +because `l` is a signed-list prefix (`deferredDrawRead_run_signed_prefix`); bound by the run-level +attempt-count fold `deferredDrawRead_run_expected_attemptCount_le`, then cancel `l.length` (run mass +`1`, `deferredDrawRead_run_neverFail`). -/ +theorem deferredDrawRead_attemptKn_mean_le {γ : Type} (pk : Stmt) (sk : Wit) + {p_abort : ℝ} (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (qSrem : ℕ) (hQ : oa.IsQueryBoundP (· matches Sum.inr _) qSrem) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + ≤ (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + classical + set run : ProbComp (γ × DeferredReadState M Commit Chal) := + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run ((((re, l), []), false), []) + with hrun + have hmass : (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run]) = 1 := by + rw [hrun] + exact tsum_probOutput_eq_one' + (deferredDrawRead_run_neverFail ids M maxAttempts pk sk oa ((((re, l), []), false), [])) + -- Recover the total combined size by adding back `l.length`; the start drawn list is empty. + have hsplit : (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + + ((l.length : ℕ) : ℝ≥0∞) + = ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞) := by + rw [show ((l.length : ℕ) : ℝ≥0∞) + = ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * ((l.length : ℕ) : ℝ≥0∞) by + rw [ENNReal.tsum_mul_right, hmass, one_mul]] + rw [← ENNReal.tsum_add] + refine tsum_congr fun z => ?_ + rw [← mul_add] + by_cases hz : z ∈ support run + · have hpre : l.length ≤ z.2.1.1.1.2.length := by + have := deferredDrawRead_run_signed_prefix ids M maxAttempts pk sk oa + ((((re, l), []), false), []) z (by rwa [hrun] at hz) + simpa using this + congr 1 + rw [← Nat.cast_add] + congr 1 + omega + · rw [probOutput_eq_zero_of_not_mem_support hz, zero_mul, zero_mul] + -- The total combined size is bounded by `l.length + qSrem/(1-p)`; cancel `l.length`. + have htot : (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + z.2.1.1.1.2.length : ℕ) : ℝ≥0∞)) + ≤ ((l.length : ℕ) : ℝ≥0∞) + (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [hrun] + have := deferredDrawRead_run_expected_attemptCount_le ids M maxAttempts pk sk hp₀ hp hAbort + oa qSrem hQ ((((re, l), []), false), []) + simpa using this + -- Subtract `l.length` from both sides of `hsplit ≤ htot` (it is a finite quantity ≤ both). + have hle : (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + + ((l.length : ℕ) : ℝ≥0∞) + ≤ ((qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort))) + ((l.length : ℕ) : ℝ≥0∞) := by + rw [hsplit, add_comm ((qSrem : ℝ≥0∞) * _)]; exact htot + exact ENNReal.le_of_add_le_add_right (by simp) hle + +omit [SampleableType Stmt] [SampleableType Chal] [DecidableEq Commit] in +/-- **Splitting an i.i.d. front draw block.** Drawing `n + m` independent commitment draws into a +list is the same computation as drawing the first `n` and then the last `m` and concatenating: the +front block factors into independent sub-blocks. This is the structural identity that, with the +i.i.d. resampling commute, lets the per-query draw blocks accumulate into one front tape. -/ +lemma drawList_commit_add (pk : Stmt) (sk : Wit) (n m : ℕ) : + OracleComp.drawList (ids.commit pk sk) (n + m) = + OracleComp.drawList (ids.commit pk sk) n >>= fun a => + OracleComp.drawList (ids.commit pk sk) m >>= fun b => pure (a ++ b) := by + classical + induction n with + | zero => simp [OracleComp.drawList] + | succ n ih => + rw [Nat.succ_add, OracleComp.drawList, OracleComp.drawList, ih] + simp only [bind_assoc, pure_bind, List.cons_append] + +omit [SampleableType Stmt] [SampleableType Chal] [DecidableEq Commit] in +/-- **Front draw blocks have a deterministic length.** Every list in the support of +`drawList (ids.commit pk sk) n` has length exactly `n`: the block always draws `n` keys. This lets +the `take`/`drop` split of an over-provisioned tape resolve to the per-query block and its +remainder. -/ +lemma length_mem_support_drawList_commit (pk : Stmt) (sk : Wit) (n : ℕ) + (ws : List (Commit × PrvState)) + (hws : ws ∈ support (OracleComp.drawList (ids.commit pk sk) n)) : + ws.length = n := by + classical + induction n generalizing ws with + | zero => + simp only [OracleComp.drawList, support_pure, Set.mem_singleton_iff] at hws + subst hws; rfl + | succ n ih => + rw [OracleComp.drawList] at hws + simp only [support_bind, support_pure, Set.mem_iUnion, Set.mem_singleton_iff] at hws + obtain ⟨w, hw, ws', hws', rfl⟩ := hws + simp [ih ws' hws'] + +/-! ### Fold-level tape factorization (reusable front-loading infrastructure) + +The body-level half of the tape factorization +(`evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody`) +recasts *one* signing body's inline attempt draws as consumption from a pre-drawn tape. The +*fold-level* half — built here — lifts that across the opaque adversary `simulateQ (oa)` fold: every +interleaved signing query's draw block commutes to the very front, so the whole run distributes as + + `drawList (ids.commit pk sk) L >>= fun tape => (simulateQ tapeDrawReadImpl oa).run (s, tape)`, + +a single independent front draw block of `L := maxAttempts · #signing-queries` commitments followed +by a tape-*consuming* run. Once the draws are front-loaded, the recorded drawn list is a function of +the tape and the value-free read list is a function of the non-tape randomness, so the read list is +independent of the tape. + +The tape-consuming handler `tapeDrawReadImpl` carries a draw tape in its state; a signing query +consumes the first `maxAttempts` tape entries (running `tapeSignBody` on them and dropping them) +instead of drawing inline, while reads/uniform behave exactly as `deferredDrawReadImpl`. The +fold equality is proved by `inductionOn oa`: at a read/uniform step the answer is independent of the +tape so the front draw block commutes trivially; at a signing step the per-body factorization +splices in the body's `drawList maxAttempts` block, which then commutes to the front of the +remaining tape via the i.i.d. resampling commute `evalDist_bind_comm_probComp`. + +This representation is an alternative to the inline (non-tape) charge that the headline uses; see +`readRecord_expected_pairs_tape_le` and `readRecord_expected_pairs_le`. -/ + +/-- The tape-consuming read-recording handler. Its state extends `DeferredReadState` with a *draw +tape* `List (Commit × PrvState)`: a signing query consumes the first `maxAttempts` entries of the +tape (running the tape-consuming body `tapeSignBody` on them and dropping them from the tape) +instead of drawing each attempt's commitment inline; uniform and random-oracle-read queries behave +exactly as `deferredDrawReadImpl` and leave the tape untouched. Over-provisioning the tape (length +`maxAttempts · #signing-queries`) makes the front-loaded draw block independent of the value-free +read list. -/ +noncomputable def tapeDrawReadImpl (pk : Stmt) (sk : Wit) : + QueryImpl ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) + (StateT (DeferredReadState M Commit Chal × List (Commit × PrvState)) ProbComp) := + fun t => match t with + | .inl (.inl n) => StateT.mk fun s => + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + | .inl (.inr mc) => StateT.mk fun s => + (fun cu => (cu.1, (((((cu.2, s.1.1.1.1.2), s.1.1.1.2), s.1.1.2 || decide (mc.2 ∈ s.1.1.1.2)), + mc.2 :: s.1.2), s.2))) <$> + roStep M s.1.1.1.1.1 mc + | .inr msg => StateT.mk fun s => + (fun alc => (alc.1.1, (((((alc.2, msg :: s.1.1.1.1.2), s.1.1.1.2 ++ alc.1.2), s.1.1.2), + s.1.2), s.2.drop maxAttempts))) <$> + (tapeSignBody ids M pk sk msg (s.2.take maxAttempts)).run s.1.1.1.1.1 + +omit [SampleableType Stmt] in +/-- **One-step unfolding of `tapeDrawReadImpl` on a uniform query.** -/ +lemma tapeDrawReadImpl_run_unif (pk : Stmt) (sk : Wit) (n : unifSpec.Domain) + (s : DeferredReadState M Commit Chal × List (Commit × PrvState)) : + (tapeDrawReadImpl ids M maxAttempts pk sk (.inl (.inl n))).run s = + (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + +omit [SampleableType Stmt] in +/-- **One-step unfolding of `tapeDrawReadImpl` on a random-oracle read query.** -/ +lemma tapeDrawReadImpl_run_read (pk : Stmt) (sk : Wit) (mc : M × Commit) + (s : DeferredReadState M Commit Chal × List (Commit × PrvState)) : + (tapeDrawReadImpl ids M maxAttempts pk sk (.inl (.inr mc))).run s = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, (((((cu.2, s.1.1.1.1.2), s.1.1.1.2), s.1.1.2 || decide (mc.2 ∈ s.1.1.1.2)), + mc.2 :: s.1.2), s.2))) <$> + roStep M s.1.1.1.1.1 mc := rfl + +omit [SampleableType Stmt] in +/-- **One-step unfolding of `tapeDrawReadImpl` on a signing query.** The body consumes the first +`maxAttempts` tape entries (via `tapeSignBody`), the drawn list is extended by the recorded rejected +commitments, and the tape advances by `maxAttempts`. -/ +lemma tapeDrawReadImpl_run_sign (pk : Stmt) (sk : Wit) (msg : M) + (s : DeferredReadState M Commit Chal × List (Commit × PrvState)) : + (tapeDrawReadImpl ids M maxAttempts pk sk (.inr msg)).run s = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((((alc.2, msg :: s.1.1.1.1.2), s.1.1.1.2 ++ alc.1.2), s.1.1.2), + s.1.2), s.2.drop maxAttempts))) <$> + (tapeSignBody ids M pk sk msg (s.2.take maxAttempts)).run s.1.1.1.1.1 := rfl + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] [DecidableEq M] in +/-- **Answer-irrelevant cross-step commute (the read/uniform inductive step).** A query step whose +answer and new non-tape state are produced by a tape-*preserving* `ProbComp` `step` (the uniform and +random-oracle-read steps both leave the tape untouched) commutes with the front draw block: pushing +the per-continuation front block to the very front past the answer is the i.i.d. resampling commute +`evalDist_bind_comm_probComp`. Given the inductive hypothesis `hcont` (the continuation run factors +as a front block followed by the tape-consuming continuation), the whole step factors likewise. -/ +theorem evalDist_tapePreserving_step_commute {γ Ans : Type} + (step : ProbComp (Ans × DeferredReadState M Commit Chal)) + (L : ℕ) + (defCont : Ans → DeferredReadState M Commit Chal → + ProbComp (γ × DeferredReadState M Commit Chal)) + (tapeCont : Ans → DeferredReadState M Commit Chal × List (Commit × PrvState) → + ProbComp (γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)))) + (pk : Stmt) (sk : Wit) + (hcont : ∀ (a : Ans) (s' : DeferredReadState M Commit Chal), + 𝒟[defCont a s'] = + 𝒟[OracleComp.drawList (ids.commit pk sk) L >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> tapeCont a (s', tape)]) : + 𝒟[step >>= fun p => defCont p.1 p.2] = + 𝒟[OracleComp.drawList (ids.commit pk sk) L >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (((fun p : Ans × DeferredReadState M Commit Chal => (p.1, (p.2, tape))) <$> step) + >>= fun p => tapeCont p.1 p.2)] := + -- A direct instance of the generic answer-irrelevant tape commute: the front draw block is the + -- `drawList (ids.commit pk sk) L` tape and `proj` discards the spent suffix. + OracleComp.DeferredSampling.evalDist_step_commute_tape step + (OracleComp.drawList (ids.commit pk sk) L) + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => (p.1, p.2.1)) + defCont tapeCont hcont + +omit [SampleableType Stmt] [DecidableEq Commit] [SampleableType Chal] in +/-- **Tape-combine reconciliation.** A front block drawn in two pieces — `maxAttempts` then +`maxAttempts · q'` — feeding a continuation `g blk rest` is the same computation as drawing the +whole `maxAttempts · (q'+1)` block at once and splitting it with `take`/`drop`: the first +`maxAttempts` entries are the body block, the remainder is the leftover tape. Uses +`drawList_commit_add` to split and the deterministic block length +`length_mem_support_drawList_commit` to resolve `take`/`drop`. -/ +theorem drawList_combine_take_drop {δ : Type} (pk : Stmt) (sk : Wit) (q' : ℕ) + (g : List (Commit × PrvState) → List (Commit × PrvState) → ProbComp δ) : + 𝒟[OracleComp.drawList (ids.commit pk sk) maxAttempts >>= fun blk => + OracleComp.drawList (ids.commit pk sk) (maxAttempts * q') >>= fun rest => g blk rest] + = 𝒟[OracleComp.drawList (ids.commit pk sk) (maxAttempts * (q' + 1)) >>= fun tape => + g (tape.take maxAttempts) (tape.drop maxAttempts)] := by + classical + rw [show maxAttempts * (q' + 1) = maxAttempts + maxAttempts * q' by ring, + drawList_commit_add ids pk sk maxAttempts (maxAttempts * q'), bind_assoc] + -- On the support of the first block, its length is `maxAttempts`, so `take`/`drop` resolve. + refine evalDist_bind_congr (fun blk hblk => ?_) + have hlen : blk.length = maxAttempts := length_mem_support_drawList_commit ids pk sk _ blk hblk + rw [bind_assoc] + refine evalDist_bind_congr_left _ _ _ (fun rest => ?_) + rw [pure_bind, List.take_left' hlen, List.drop_left' hlen] + +omit [SampleableType Stmt] in +theorem evalDist_defSignStep_splice {δ : Type} (pk : Stmt) (sk : Wit) (msg : M) + (s : DeferredReadState M Commit Chal) + (k : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache → + ProbComp δ) : + 𝒟[(ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 >>= k] = + 𝒟[OracleComp.drawList (ids.commit pk sk) maxAttempts >>= fun blk => + (tapeSignBody ids M pk sk msg blk).run s.1.1.1.1 >>= k] := by + rw [show (OracleComp.drawList (ids.commit pk sk) maxAttempts >>= fun blk => + (tapeSignBody ids M pk sk msg blk).run s.1.1.1.1 >>= k) + = (OracleComp.drawList (ids.commit pk sk) maxAttempts >>= fun blk => + (tapeSignBody ids M pk sk msg blk).run s.1.1.1.1) >>= k from by rw [bind_assoc]] + rw [evalDist_bind, + evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody ids M pk sk msg maxAttempts s.1.1.1.1, + ← evalDist_bind] + +omit [SampleableType Stmt] in +/-- **Sign-step cross-step commute (the crux inductive step).** The deferred-draw sign step, +composed with the deferred continuation, factors as a single front draw block of +`maxAttempts·(q'+1)` commitments followed by the tape-consuming sign step + tape continuation. The +genuine framework content: the body's `maxAttempts` draw block splices to the front via the per-body +factorization (`evalDist_defSignStep_splice`); the continuation's `maxAttempts·q'` block (supplied +by the inductive hypothesis `hcont`) commutes past the body via the i.i.d. resampling commute +(`evalDist_bind_comm_probComp`); the two blocks combine into one `maxAttempts·(q'+1)` block split by +`take`/`drop` (`drawList_combine_take_drop`), exactly the tape the tape-consuming sign step +consumes. -/ +theorem evalDist_signStep_commute {γ : Type} (pk : Stmt) (sk : Wit) (msg : M) + (s : DeferredReadState M Commit Chal) (q' : ℕ) + (ob : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (Sum.inr msg) → + OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (hcont : ∀ (a : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) + (s' : DeferredReadState M Commit Chal), + 𝒟[(simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob a)).run s'] = + 𝒟[OracleComp.drawList (ids.commit pk sk) (maxAttempts * q') >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob a)).run (s', tape)]) : + 𝒟[(deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s >>= fun p => + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2] = + 𝒟[OracleComp.drawList (ids.commit pk sk) (maxAttempts * (q' + 1)) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + ((tapeDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run (s, tape) >>= fun p => + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2)] := by + classical + -- LHS: fold the deferred sign step's map into the body bind, then splice the front block. + rw [show (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s >>= (fun p => + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2) + = (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 >>= fun alc => + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2) + from by + rw [show (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s + = (fun alc : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1 from rfl] + simp [bind_map_left]] + rw [evalDist_defSignStep_splice ids M maxAttempts pk sk msg s + (fun alc => (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2))] + -- Rewrite the continuation by `hcont`, under the leading `drawList maxAttempts` and body binds. + rw [evalDist_bind_congr (mx := OracleComp.drawList (ids.commit pk sk) maxAttempts) + (fun blk _ => evalDist_bind_congr (mx := (tapeSignBody ids M pk sk msg blk).run s.1.1.1.1) + (fun alc _ => hcont alc.1.1 ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)))] + -- Commute the continuation's `maxAttempts·q'` block to the front past the body. + rw [evalDist_bind_congr (mx := OracleComp.drawList (ids.commit pk sk) maxAttempts) + (fun blk _ => evalDist_bind_comm_probComp ((tapeSignBody ids M pk sk msg blk).run s.1.1.1.1) + (OracleComp.drawList (ids.commit pk sk) (maxAttempts * q')) + (fun alc tape => (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run + (((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2), tape)))] + -- Combine the two front blocks into one `maxAttempts·(q'+1)` block split by `take`/`drop`. + rw [drawList_combine_take_drop ids maxAttempts pk sk q' + (fun blk rest => (tapeSignBody ids M pk sk msg blk).run s.1.1.1.1 >>= fun alc => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run + (((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2), rest))] + -- Match the RHS: the tape sign step consumes `take maxAttempts` and threads `drop maxAttempts`. + refine evalDist_bind_congr_left _ _ _ (fun tape => ?_) + rw [show (tapeDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run (s, tape) + = (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, (((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2), + tape.drop maxAttempts))) <$> + (tapeSignBody ids M pk sk msg (tape.take maxAttempts)).run s.1.1.1.1 from rfl] + simp [bind_map_left, map_bind] + +omit [SampleableType Stmt] in +/-- **The fold-level tape factorization (the framework lemma).** By induction on the adversary +computation `oa`, the read-recording deferred-draw run distributes as a single front draw block of +`maxAttempts · qSrem` commitments followed by a tape-consuming run: + +`𝒟[(simulateQ deferredDrawReadImpl oa).run s]` +` = 𝒟[drawList (ids.commit pk sk) (maxAttempts · qSrem) >>= fun tape =>` +` (simulateQ tapeDrawReadImpl oa).run (s, tape)]`, + +where `qSrem` bounds the number of signing queries of `oa` (the `(· matches .inr _)` component of +`signHashQueryBound`). The tape is over-provisioned (length `maxAttempts · qSrem`); each signing +query consumes its `maxAttempts`-prefix and the unused suffix is discarded on early accept. + +The proof inducts on `oa`. At a **read/uniform** step the query answer is independent of the tape, +so the front draw block commutes past it (the i.i.d. resampling commute +`evalDist_bind_comm_probComp`), +matching the inductive hypothesis for the continuation. At a **signing** step the per-body +factorization `evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody` recasts the body's inline draws +as a `drawList maxAttempts` block; that block is split off the front via `drawList_commit_add` (the +remaining `maxAttempts · (qSrem-1)` block feeding the continuation by the inductive hypothesis) and +commuted to the front past the answer-irrelevant continuation. The general principle it instantiates +is that answer-irrelevant per-step draws factor to a front tape in `simulateQ`. -/ +theorem evalDist_deferredDrawRead_eq_drawList_tapeDrawRead {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) : + ∀ (qSrem : ℕ), oa.IsQueryBoundP (· matches Sum.inr _) qSrem → + ∀ (s : DeferredReadState M Commit Chal), + 𝒟[(simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] = + 𝒟[OracleComp.drawList (ids.commit pk sk) (maxAttempts * qSrem) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) oa).run (s, tape)] := by + classical + induction oa using OracleComp.inductionOn with + | pure a => + intro qSrem _ s + simp only [simulateQ_pure, StateT.run_pure, map_pure] + rw [evalDist_bind_const_neverFails _ (OracleComp.probFailure_drawList _ _)] + | query_bind t ob ih => + intro qSrem hQ s + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + rcases t with (n | mc) | msg + · -- UNIFORM: the answer is independent of the tape; commute the front block past the draw. + have hqs : (if (match (Sum.inl (Sum.inl n) : + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) with + | Sum.inr _ => true | _ => false) = true then qSrem - 1 else qSrem) = qSrem := rfl + rw [hqs] at hQ2 + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + rw [show (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run s + = (fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n + from rfl] + -- The tape uniform step is the deferred step with the tape inserted (`Functor.map_map`). + rw [show (fun tape => (fun p : + γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + ((tapeDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run (s, tape) + >>= fun p => + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2)) + = (fun tape => (fun p : + γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (((fun p : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inl (Sum.inl n))) × + DeferredReadState M Commit Chal => (p.1, (p.2, tape))) <$> + ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n)) + >>= fun p => + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2)) + from by funext tape; rw [tapeDrawReadImpl_run_unif, Functor.map_map]; rfl] + exact evalDist_tapePreserving_step_commute ids M + ((fun u => (u, s)) <$> (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) + (maxAttempts * qSrem) + (fun a s' => (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob a)).run s') + (fun a st => (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob a)).run st) + pk sk (fun a s' => ih a qSrem (hQ2 a) s') + · -- READ: the answer is `roStep` (real layer), independent of the tape; same commute. + have hqs : (if (match (Sum.inl (Sum.inr mc) : + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) with + | Sum.inr _ => true | _ => false) = true then qSrem - 1 else qSrem) = qSrem := rfl + rw [hqs] at hQ2 + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + rw [show (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inr mc))).run s + = (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc + from rfl] + rw [show (fun tape => (fun p : + γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + ((tapeDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inr mc))).run (s, tape) + >>= fun p => + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2)) + = (fun tape => (fun p : + γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (((fun p : Chal × DeferredReadState M Commit Chal => (p.1, (p.2, tape))) <$> + ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc)) + >>= fun p => + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob p.1)).run p.2)) + from by funext tape; rw [tapeDrawReadImpl_run_read, Functor.map_map]; rfl] + exact evalDist_tapePreserving_step_commute ids M + ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc) + (maxAttempts * qSrem) + (fun a s' => (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob a)).run s') + (fun a st => (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) (ob a)).run st) + pk sk (fun a s' => ih a qSrem (hQ2 a) s') + · -- SIGN: the crux. Splice the per-body draw block to the front past the continuation. + have hpos : 0 < qSrem := by + rcases hQ1 with h | h + · exact absurd rfl h + · exact h + clear hQ1 + have hqs : (if (match (Sum.inr msg : + ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Domain) with + | Sum.inr _ => true | _ => false) = true then qSrem - 1 else qSrem) = qSrem - 1 := rfl + rw [hqs] at hQ2 + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + rw [show qSrem = (qSrem - 1) + 1 from by omega] + exact evalDist_signStep_commute ids M maxAttempts pk sk msg s (qSrem - 1) ob + (fun a s' => ih a (qSrem - 1) (hQ2 a) s') + +omit [SampleableType Stmt] in +/-- **The atomic value-free charge (the irreducible probabilistic kernel).** One fresh raw +commitment draw `w ← ids.commit pk sk`, *independent of* a value-free list `rl`, contributes +expected multiplicity `E[rl.count w.1] ≤ ε · rl.length`: each of the `rl.length` slots of `rl` is +hit by the fresh draw with probability `Pr[= slot | Prod.fst <$> ids.commit pk sk] ≤ ε` (`hGuess`). + +This is the single source of the `ε` in the ghost-read bound. It is purely the per-draw mass bound +combined with the independence of the draw from the (value-free) read list; the structural content +of the full charge is to exhibit each recorded rejected draw of the tape run in exactly this +independent-of-the-readlist position (the value-substitution at rejected tape positions). -/ +private lemma tsum_probOutput_commit_mul_count_le {C P : Type} [DecidableEq C] + (commit : ProbComp (C × P)) (rl : List C) (ε : ℝ) + (hGuess : ∀ cm : C, Pr[= cm | Prod.fst <$> commit] ≤ ENNReal.ofReal ε) : + (∑' w : C × P, Pr[= w | commit] * (rl.count w.1 : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * (rl.length : ℝ≥0∞) := + OracleComp.DeferredSampling.tsum_probOutput_fresh_mul_count_le commit rl ε hGuess + +omit [SampleableType Stmt] in +/-- **Value-substitution: the recorded read list is independent of the drawn-list content.** The +expected multiplicity `E[readlist.count w]` of any fixed commitment `w` in the recorded read list of +the read-recording run depends only on the start *real cache*, *signed list*, and *read list* — not +on the start *drawn list* `D` nor the start *bad flag* `b`. This is the structural value-freeness at +the heart of the ghost-read bound: the recorded reads answer via `roStep` on the real layer and +never the drawn (rejected) values, so changing the drawn list (or the bad flag, which is write-only +and never gates control flow) leaves the read-list marginal unchanged. + +Formally the expectation is invariant under both drawn-list and bad-flag start values. Proved by +induction on `oa`: +* **pure** — the read list is the start one (independent of `D`, `b`). +* **uniform** — the draw is forwarded and the drawn list / bad flag / read list are untouched; the + inductive hypothesis applies to the unchanged-`D` continuation. +* **read** — the read list grows by exactly `mc.2` (the same regardless of `D`); the bad flag + updates to `b || (mc.2 ∈ D)` (which *does* depend on `D`), but since the inductive hypothesis is + quantified over *all* bad-flag values, the two `D`-runs still agree. +* **sign** — the body draws are the same regardless of `D`, `b`; the drawn list grows by the body's + rejected commitments and the bad flag is preserved, and the inductive hypothesis (quantified over + all `D`) closes the differing-drawn-list continuations. -/ +theorem deferredDrawRead_run_count_dl_invariant {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (w : Commit) (re : (M × Commit →ₒ Chal).QueryCache) (sgn : List M) + (rl : List Commit) : + ∀ (D₁ : List Commit) (b₁ : Bool) (D₂ : List Commit) (b₂ : Bool), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D₁), b₁), rl)] * (z.2.2.count w : ℝ≥0∞)) + = ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D₂), b₂), rl)] * (z.2.2.count w : ℝ≥0∞) := by + induction oa using OracleComp.inductionOn generalizing re sgn rl with + | pure a => + intro D₁ b₁ D₂ b₂ + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + | query_bind t ob ih => + intro D₁ b₁ D₂ b₂ + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind, tsum_probOutput_bind_mul] + rcases t with (n | mc) | msg + · -- UNIFORM: drawn list / bad flag / read list untouched; forward draw. + set G : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (Sum.inl (Sum.inl n))) × DeferredReadState M Commit Chal → ℝ≥0∞ := + fun x => ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (z.2.2.count w : ℝ≥0∞) with hG + have hx₁ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run + ((((re, sgn), D₁), b₁), rl)) = + (fun u => (u, ((((re, sgn), D₁), b₁), rl))) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + have hx₂ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inl n))).run + ((((re, sgn), D₂), b₂), rl)) = + (fun u => (u, ((((re, sgn), D₂), b₂), rl))) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n := rfl + rw [hx₁, hx₂] + refine (tsum_probOutput_map_mul _ _ G).trans + ((tsum_congr fun u => ?_).trans (tsum_probOutput_map_mul _ _ G).symm) + exact congrArg _ (ih u re sgn rl D₁ b₁ D₂ b₂) + · -- READ: read list grows by `mc.2` (independent of `D`); the bad flag updates to + -- `b || (mc.2 ∈ D)` (D-dependent), but `ih` is quantified over *all* bad flags. + set G : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (Sum.inl (Sum.inr mc))) × DeferredReadState M Commit Chal → ℝ≥0∞ := + fun x => ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (z.2.2.count w : ℝ≥0∞) with hG + have hx₁ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inr mc))).run + ((((re, sgn), D₁), b₁), rl)) = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, sgn), D₁), b₁ || decide (mc.2 ∈ D₁)), mc.2 :: rl))) <$> + roStep M re mc := rfl + have hx₂ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inl (Sum.inr mc))).run + ((((re, sgn), D₂), b₂), rl)) = + (fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, sgn), D₂), b₂ || decide (mc.2 ∈ D₂)), mc.2 :: rl))) <$> + roStep M re mc := rfl + rw [hx₁, hx₂] + refine (tsum_probOutput_map_mul _ _ G).trans + ((tsum_congr fun cu => ?_).trans (tsum_probOutput_map_mul _ _ G).symm) + exact congrArg _ + (ih cu.1 cu.2 sgn (mc.2 :: rl) D₁ (b₁ || decide (mc.2 ∈ D₁)) D₂ + (b₂ || decide (mc.2 ∈ D₂))) + · -- SIGN: the body draws are `D`-independent; drawn list grows by the body's rejected + -- commitments and the bad flag is preserved; `ih` (over all `D`) closes the continuations. + set G : (((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range + (Sum.inr msg)) × DeferredReadState M Commit Chal → ℝ≥0∞ := + fun x => ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (z.2.2.count w : ℝ≥0∞) with hG + have hx₁ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run + ((((re, sgn), D₁), b₁), rl)) = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: sgn), D₁ ++ alc.1.2), b₁), rl))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run re := rfl + have hx₂ : ((deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run + ((((re, sgn), D₂), b₂), rl)) = + (fun alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache => + (alc.1.1, ((((alc.2, msg :: sgn), D₂ ++ alc.1.2), b₂), rl))) <$> + (ghostSignDrawBody ids M pk sk msg maxAttempts).run re := rfl + rw [hx₁, hx₂] + refine (tsum_probOutput_map_mul _ _ G).trans + ((tsum_congr fun alc => ?_).trans (tsum_probOutput_map_mul _ _ G).symm) + exact congrArg _ + (ih alc.1.1 alc.2 (msg :: sgn) rl (D₁ ++ alc.1.2) b₁ (D₂ ++ alc.1.2) b₂) + +omit [SampleableType Stmt] in +/-- **The value-substituted continuation read-multiplicity functional is drawn-invariant.** A +restatement of `deferredDrawRead_run_count_dl_invariant` reorganised for the body charge: the +expected read-multiplicity `E[Σ_{rc ∈ readlist} R.count rc]` of a *fixed* commit list `R` against +the continuation's recorded read list is invariant under the continuation's start drawn list (and +bad flag). The reads answer via `roStep` on the real layer, never the drawn (rejected) values, so +adding `R` (or any list) to the start drawn list does not change the read-list marginal. -/ +theorem deferredDrawRead_run_sum_count_dl_invariant {γ : Type} (pk : Stmt) (sk : Wit) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (R : List Commit) (re : (M × Commit →ₒ Chal).QueryCache) (sgn : List M) + (rl : List Commit) (D₁ : List Commit) (b₁ : Bool) (D₂ : List Commit) (b₂ : Bool) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D₁), b₁), rl)] * ((R.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + = ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D₂), b₂), rl)] * ((R.map (fun w => z.2.2.count w)).sum : ℝ≥0∞) := by + classical + induction R with + | nil => simp + | cons w R ih => + simp only [List.map_cons, List.sum_cons, Nat.cast_add] + rw [show ∀ (D : List Commit) (b : Bool), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D), b), rl)] * + (((z.2.2.count w : ℕ) : ℝ≥0∞) + ((R.map (fun w => z.2.2.count w)).sum : ℝ≥0∞))) + = (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D), b), rl)] * ((z.2.2.count w : ℕ) : ℝ≥0∞)) + + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, sgn), D), b), rl)] * + ((R.map (fun w => z.2.2.count w)).sum : ℝ≥0∞) from + fun D b => by rw [← ENNReal.tsum_add]; exact tsum_congr fun z => by rw [mul_add]] + rw [deferredDrawRead_run_count_dl_invariant ids M maxAttempts pk sk oa w re sgn rl + D₁ b₁ D₂ b₂, ih, ← ENNReal.tsum_add] + exact tsum_congr fun z => by rw [mul_add] + +omit [SampleableType Stmt] in +/-- **One step of the constant-length body charge (the genuine per-attempt induction step).** The +`succ` case of `ghostSignDrawBody_continuation_charge`: peel the head commit draw `ws` (kept +*averaged* — the head `ε`-kernel needs the full `ids.commit` marginal, a per-`ws` bound is false), +the challenge and the response, and case on the accept/reject branch. On *accept* the body records +nothing (charge `0`). On *reject* the recorded rejects are `ws.1 :: rec-rejects`; the +read-multiplicity splits into the head `z.readlist.count ws.1` (paid by the unconditional `+1` via +the value-substituted, gate-dropped marginal `ε`-kernel) and the recursive body charge (the +inductive hypothesis `ih` at the extended start drawn list `dr ++ [ws.1]`). The body never fails, so +the full-mass identities make the head `≤ L₀` match the RHS `+1`. -/ +theorem ghostSignDrawBody_succ_charge {γ : Type} + (qH : ℕ) (ε : ℝ) (_hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (msg : M) + (ob : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg) → + OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (hob : ∀ u, (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH) + (sgn : List M) (rl : List Commit) (bad : Bool) (n : ℕ) + (re : (M × Commit →ₒ Chal).QueryCache) (dr : List Commit) + (ih : ∀ (re : (M × Commit →ₒ Chal).QueryCache) (dr : List Commit), + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((rl.length + qH : ℕ) : ℝ≥0∞) * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞)) : + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg (n + 1)).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((rl.length + qH : ℕ) : ℝ≥0∞) * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg (n + 1)).run re] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) := by + classical + set L₀ : ℝ≥0∞ := ENNReal.ofReal ε * ((rl.length + qH : ℕ) : ℝ≥0∞) with hL₀ + -- The continuation run never fails, so its output mass is `1`. + have hcontMass : ∀ (u : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) + (cache : (M × Commit →ₒ Chal).QueryCache) (D : List Commit), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob u)).run + ((((cache, sgn), D), bad), rl)]) = 1 := fun u cache D => + tsum_probOutput_eq_one' + (deferredDrawRead_run_neverFail ids M maxAttempts pk sk (ob u) _) + -- The signing body never fails, so its output mass is `1`. + have hbodyMass : ∀ (re' : (M × Commit →ₒ Chal).QueryCache), + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg n).run re']) = 1 := by + intro re' + exact tsum_probOutput_eq_one' (by simp) + -- Deterministic continuation-readlist bound: every continuation run started at read list `rl` + -- with read budget `qH` records `≤ rl.length + qH` reads. + have hlen : ∀ (u : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) + (cache : (M × Commit →ₒ Chal).QueryCache) (D : List Commit) + (z' : γ × DeferredReadState M Commit Chal), + z' ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob u)).run + ((((cache, sgn), D), bad), rl)) → + (z'.2.2.length : ℝ≥0∞) ≤ ((rl.length + qH : ℕ) : ℝ≥0∞) := by + intro u cache D z' hz' + have := deferredDrawReadImpl_run_readlist_length_le ids M maxAttempts pk sk (ob u) qH + (hob u) ((((cache, sgn), D), bad), rl) z' hz' + exact_mod_cast this + -- Per-`ws` value-substituted ungated head charge. + set H : Commit × PrvState → ℝ≥0∞ := fun ws => + ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob rws.1.1)).run + ((((rws.2, sgn), dr), bad), rl)] * ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞) with hH + -- Per-`ws` recorded-length factor of the one-attempt body (RHS length factor minus the `+1`). + set R : Commit × PrvState → ℝ≥0∞ := fun ws => + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length : ℕ) : ℝ≥0∞) with hR + -- The per-`ws` head bound, summed over `ws` (gate dropped, value-substituted, `ε`-kernel). + have hHead : (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * H ws) ≤ L₀ := by + rw [hH] + -- Per `(rws, z)` the inner `ws`-marginal of `z.count ws.1` is `≤ L₀`; the body and continuation + -- have full mass, so the whole head expectation is `≤ L₀`. + have hinner : ∀ (rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache) + (z : γ × DeferredReadState M Commit Chal), + z ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob rws.1.1)).run + ((((rws.2, sgn), dr), bad), rl)) → + (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) + ≤ L₀ := by + intro rws z hz + calc (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((z.2.2.length : ℕ) : ℝ≥0∞) := + tsum_probOutput_commit_mul_count_le (ids.commit pk sk) z.2.2 ε (fun cm => hGuess cm) + _ ≤ L₀ := by rw [hL₀]; gcongr; exact_mod_cast hlen rws.1.1 rws.2 dr z hz + -- Rewrite the head as a single average over `(ws, rws, z)`, reorder, bound, and recombine. + calc (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] * + ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) + = ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] * + (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) := by + -- Fully distribute the probability weights, reorder `ws` innermost, recombine. + simp_rw [← ENNReal.tsum_mul_left] + rw [ENNReal.tsum_comm] + refine tsum_congr fun rws => ?_ + rw [ENNReal.tsum_comm] + refine tsum_congr fun z => ?_ + refine tsum_congr fun ws => by ring + _ ≤ ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] * L₀ := by + refine ENNReal.tsum_le_tsum fun rws => ?_ + refine mul_le_mul' le_rfl ?_ + refine ENNReal.tsum_le_tsum fun z => ?_ + rcases eq_or_ne Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] 0 with hz | hz + · rw [hz]; simp + · gcongr + exact hinner rws z ((mem_support_iff _ _).mpr hz) + _ = L₀ * ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] := by + rw [← ENNReal.tsum_mul_left] + refine tsum_congr fun rws => ?_ + rw [ENNReal.tsum_mul_right, ← mul_assoc, mul_comm _ L₀, mul_assoc] + _ = L₀ := by + have hone : (∑' rws : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)]) = 1 := by + rw [← hbodyMass re] + exact tsum_congr fun rws => by rw [hcontMass rws.1.1 rws.2 dr, mul_one] + rw [hone, mul_one] + -- The per-`ws` LHS inner bound: head + recursive (the inductive hypothesis). + have h_ws : ∀ ws : Commit × PrvState, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ H ws + L₀ * R ws := by + intro ws + -- Body-`n` expected `length + 1` (the reject-branch length factor; `+1` is the head commit). + set Rr : ℝ≥0∞ := + ∑' rws : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= rws | (ghostSignDrawBody ids M pk sk msg n).run re] * + ((rws.1.2.length + 1 : ℕ) : ℝ≥0∞) with hRr + -- `R ws = Pr[reject ws] · Rr` (accept records length `0`; reject records `ws.1 :: rws`). + have hR_eq : R ws = Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + Rr := by + rw [hR, probOutput_bind_eq_tsum, ← ENNReal.tsum_mul_right] + simp only [] + rw [tsum_probOutput_bind_mul] + refine tsum_congr fun ch => ?_ + rw [tsum_probOutput_bind_mul] + -- Per response `oz`: accept records length `0`; reject records `(ws.1 :: rws).length`. + have h_oz : ∀ oz : Option Resp, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * ((alc.1.2.length : ℕ) : ℝ≥0∞)) + = (if oz = none then Rr else 0) := by + intro oz + cases oz with + | some z => rw [if_neg (by simp), tsum_probOutput_pure_mul]; simp + | none => + rw [if_pos rfl, hRr, map_eq_bind_pure_comp, tsum_probOutput_bind_mul] + refine tsum_congr fun rws => ?_ + simp only [Function.comp] + rw [tsum_probOutput_pure_mul] + simp [List.length_cons] + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by + rw [h_oz oz, if_neg hoz, mul_zero]] + rw [h_oz none, if_pos rfl]; ring + -- Peel the challenge `ch`. On *accept* the recorded list is empty (charge `0`); only the + -- *reject* branch contributes, gated by `Pr[none | respond]`. + rw [tsum_probOutput_bind_mul] + -- Per-challenge: peel the response, then case on accept/reject. + have h_ch : ∀ ch : Chal, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ Pr[= none | ids.respond pk sk ws.2 ch] * (H ws + L₀ * Rr) := by + intro ch + rw [tsum_probOutput_bind_mul] + -- Per response `oz`: accept records nothing (charge `0`); reject = head + recursion. + have h_oz : ∀ oz : Option Resp, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ (if oz = none then H ws + L₀ * Rr else 0) := by + intro oz + cases oz with + | some z => + rw [if_neg (by simp), tsum_probOutput_pure_mul] + simp + | none => + rw [if_pos rfl, map_eq_bind_pure_comp, tsum_probOutput_bind_mul] + -- The reject branch: split the recorded count list `ws.1 :: rws.1.2` into head + tail. + have hsplit : ∀ rws : (Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (pure ((rws.1.1, ws.1 :: rws.1.2), rws.2) : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + = (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), dr), bad), rl)] * + ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) + + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run + ((((rws.2, sgn), (dr ++ [ws.1]) ++ rws.1.2), bad), rl)] * + ((rws.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞) := by + intro rws + rw [tsum_probOutput_pure_mul] + simp only [List.map_cons, List.sum_cons, Nat.cast_add] + rw [show dr ++ ws.1 :: rws.1.2 = (dr ++ [ws.1]) ++ rws.1.2 from by simp] + rw [show (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run ((((rws.2, sgn), (dr ++ [ws.1]) ++ rws.1.2), bad), rl)] * + ((z.2.2.count ws.1 : ℝ≥0∞) + + ((rws.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞))) + = (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run + ((((rws.2, sgn), (dr ++ [ws.1]) ++ rws.1.2), bad), rl)] * + ((z.2.2.count ws.1 : ℕ) : ℝ≥0∞)) + + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob rws.1.1)).run + ((((rws.2, sgn), (dr ++ [ws.1]) ++ rws.1.2), bad), rl)] * + ((rws.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞) from by + rw [← ENNReal.tsum_add]; exact tsum_congr fun z => by rw [mul_add]] + -- Head: value-substitute the drawn list from `(dr ++ [ws.1]) ++ rws.1.2` to `dr`. + congr 1 + exact deferredDrawRead_run_count_dl_invariant ids M maxAttempts pk sk (ob rws.1.1) + ws.1 rws.2 sgn rl ((dr ++ [ws.1]) ++ rws.1.2) bad dr bad + -- Now `h_oz none` reduces to: `∑'rws Pr[rws]·(head + rec) ≤ H ws + L₀·Rr`. + simp only [Function.comp] + simp_rw [hsplit, mul_add] + rw [ENNReal.tsum_add] + -- The head sum *is* `H ws`; the recursive sum is bounded by the inductive hypothesis. + refine add_le_add (le_of_eq ?_) ?_ + · rw [hH] + · -- `dr ++ [ws.1]` form matches the inductive hypothesis at the extended prefix. + rw [hRr] + refine le_trans ?_ (ih re (dr ++ [ws.1])) + exact le_of_eq (tsum_congr fun x => by rw [List.append_assoc]) + -- Sum over `oz`: only the reject (`none`) term survives, gated by `Pr[none | respond]`. + calc (∑' oz : Option Resp, Pr[= oz | ids.respond pk sk ws.2 ch] * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re : + ProbComp ((Option (Commit × Resp) × List Commit) × + (M × Commit →ₒ Chal).QueryCache))] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ∑' oz : Option Resp, Pr[= oz | ids.respond pk sk ws.2 ch] * + (if oz = none then H ws + L₀ * Rr else 0) := + ENNReal.tsum_le_tsum fun oz => by gcongr; exact h_oz oz + _ = Pr[= none | ids.respond pk sk ws.2 ch] * (H ws + L₀ * Rr) := by + rw [tsum_eq_single (none : Option Resp) fun oz hoz => by rw [if_neg hoz, mul_zero]] + rw [if_pos rfl] + -- Sum over `ch`: factor out the reject probability and fold via `hR_eq`. + calc (∑' ch : Chal, Pr[= ch | uniformSample Chal] * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ∑' ch : Chal, Pr[= ch | uniformSample Chal] * + (Pr[= none | ids.respond pk sk ws.2 ch] * (H ws + L₀ * Rr)) := + ENNReal.tsum_le_tsum fun ch => by gcongr; exact h_ch ch + _ = (∑' ch : Chal, Pr[= ch | uniformSample Chal] * + Pr[= none | ids.respond pk sk ws.2 ch]) * (H ws + L₀ * Rr) := by + rw [← ENNReal.tsum_mul_right]; exact tsum_congr fun ch => (mul_assoc _ _ _).symm + _ = Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] * + (H ws + L₀ * Rr) := by rw [probOutput_bind_eq_tsum] + _ ≤ H ws + L₀ * R ws := by + rw [mul_add, hR_eq] + refine add_le_add (mul_le_of_le_one_left zero_le probOutput_le_one) ?_ + rw [← mul_assoc, mul_comm + Pr[= none | uniformSample Chal >>= fun ch => ids.respond pk sk ws.2 ch] L₀, mul_assoc] + -- Assemble: unfold the `succ` body, peel the commit draw, apply `h_ws`, and split the sums. + rw [run_ghostSignDrawBody_succ, tsum_probOutput_bind_mul] + rw [tsum_probOutput_bind_mul] + -- RHS inner equals `R ws + 1` (the body never fails, so the `+1` carries full mass). + have hRinner : ∀ ws : Commit × PrvState, + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞)) + = R ws + 1 := by + intro ws + have hmass : (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re]) = 1 := + tsum_probOutput_eq_one' (by simp) + rw [hR] + simp only [Nat.cast_add, Nat.cast_one] + rw [show (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length : ℝ≥0∞) + 1)) + = (∑' alc, Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * (alc.1.2.length : ℝ≥0∞)) + + ∑' alc, Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] from by + rw [← ENNReal.tsum_add]; exact tsum_congr fun alc => by rw [mul_add, mul_one]] + rw [hmass] + calc (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * (H ws + L₀ * R ws) := + ENNReal.tsum_le_tsum fun ws => by gcongr; exact h_ws ws + _ = (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * H ws) + + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * (L₀ * R ws) := by + rw [← ENNReal.tsum_add]; exact tsum_congr fun ws => by rw [mul_add] + _ ≤ L₀ + ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * (L₀ * R ws) := by + gcongr + _ = L₀ * ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | uniformSample Chal >>= fun ch => + ids.respond pk sk ws.2 ch >>= fun oz => + match oz with + | some z => pure ((some (ws.1, z), []), re.cacheQuery (msg, ws.1) ch) + | none => (fun rws => ((rws.1.1, ws.1 :: rws.1.2), rws.2)) <$> + (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) := by + have hcommitMass : (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk]) = 1 := + tsum_probOutput_eq_one' (by simp) + simp_rw [hRinner, mul_add, mul_one] + rw [ENNReal.tsum_add, hcommitMass, mul_add, mul_one] + rw [show (∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * (L₀ * R ws)) + = L₀ * ∑' ws : Commit × PrvState, Pr[= ws | ids.commit pk sk] * R ws from by + rw [← ENNReal.tsum_mul_left]; exact tsum_congr fun ws => by ring] + rw [add_comm] + +omit [SampleableType Stmt] in +/-- **The constant-length body charge (the genuine per-attempt induction).** Over one signing body +`ghostSignDrawBody n`, the expected continuation read-multiplicity of the body's *rejected* draws — +`E[Σ_{w ∈ body-rejects} continuation.readlist.count w]` — is at most `ε · (rl.length + qH) · +E[#body-rejects + 1]`, where `rl` is the continuation's start read list and `qH` the continuation's +read-query budget (so the continuation's recorded read list has length `≤ rl.length + qH` +deterministically). The `+1` is the body's single unconditional signing query; it is *not* slack — +it pays the reject-gate skew of the head charge (see below). + +Proved by induction on `n`: +* **0** — the body rejects nothing, the read-multiplicity is `0 ≤ ε · (rl.length + qH) · 1`. +* **n+1** — peel the head commit draw `ws` (kept *averaged*: the `ε`-kernel needs the full + `ids.commit` marginal — a per-`ws` bound is false, the adversary could target a fixed `ws.1`), + the challenge, the response, and case on the accept/reject branch. On *accept* the body records + nothing (`rej = []`, charge `0`). On *reject* the recorded rejects are `ws.1 :: rec-rejects`; the + read-multiplicity `Σ_{w ∈ ws.1 :: rec-rejects} z'.readlist.count w` splits as + `z'.readlist.count ws.1` (head) plus the recursive body charge (recurses to the inductive + hypothesis at the extended start drawn list `dr ++ [ws.1]`). + + Crucially the two halves treat the reject gate `1[respond = none]` differently: + * the **head** charge `Σ_{ws} commit(ws) · 1[reject(ws.2)] · z'.readlist.count ws.1` drops the + gate (`1[reject] ≤ 1`) — necessary because `ws.1` and the reject decision `f(ws.2, c)` are + *correlated* (the prover state `ws.2` determines both the commit and the accept decision), so a + gated kernel would skew the `ws.1` marginal. After value-substitution + (`deferredDrawRead_run_sum_count_dl_invariant` moves `ws.1` out of the continuation's drawn + list) and the marginal `ε`-kernel `tsum_probOutput_commit_mul_count_le`, the ungated head is + `≤ ε · z'.readlist.length ≤ ε · (rl.length + qH)`, paid by the unconditional `+1`; + * the **recursive** charge `Σ_{ws} commit(ws) · 1[reject(ws.2)] · (rec body charge)` *keeps* the + gate, so it is `Pr[reject] · ε · (rl.length + qH) · E[#rec-rejects + 1]` (inductive hypothesis), + which the reject paths of `#body-rejects` in the right-hand side exactly cover. Dropping the + recursive gate would be unsound (it over-charges by the accept mass). -/ +theorem ghostSignDrawBody_continuation_charge {γ : Type} + (qH : ℕ) (ε : ℝ) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (msg : M) + (ob : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg) → + OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (hob : ∀ u, (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH) + (sgn : List M) (rl : List Commit) (bad : Bool) : + ∀ (n : ℕ) (re : (M × Commit →ₒ Chal).QueryCache) (dr : List Commit), + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg n).run re] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, sgn), dr ++ alc.1.2), bad), rl)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((rl.length + qH : ℕ) : ℝ≥0∞) * + ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg n).run re] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) := by + classical + intro n + induction n with + | zero => + intro re dr + simp only [ghostSignDrawBody, StateT.run_pure, tsum_probOutput_pure_mul, List.map_nil, + List.sum_nil, Nat.cast_zero, mul_zero, tsum_zero] + exact zero_le + | succ n ih => + intro re dr + -- The genuine per-attempt step; see `ghostSignDrawBody_succ_charge`. + exact ghostSignDrawBody_succ_charge ids M maxAttempts qH ε hε pk sk hGuess msg ob hob sgn rl + bad n re dr ih + + +omit [SampleableType Stmt] in +/-- **The sign-step value-free charge — the probabilistic core of the ghost-read bound.** +This is the single-query step of the inline-run induction +`readRecord_expected_pairs_nontape_general` at a *signing* query `Sum.inr msg`. The signing body +`ghostSignDrawBody` draws `maxAttempts` fresh commitments inline, records the *rejected* ones into +the drawn list, and runs the continuation `ob` from the post-body state; the goal bounds the +resulting expected pair count by the `s`-based pre-existing term plus `ε` times the `s`-based +new-attempt count. + +The genuine content is concentrated here. Expanding the inductive hypothesis at the post-body state, +the only term not covered by the `s`-based pre-existing term and the slack of the `#attempt` count +is the **body charge** `E[Σ_{rc ∈ readlist} body-rejects.count rc]`, which must be bounded by +`ε · E[readlist.length · #body-attempts]` (where `#body-attempts = #body-rejects + 1`, the body's +single unconditional signing query providing the `+1`). Crucially the body's draws must remain +**averaged** (the sum over body outputs is retained, not factored): for a *fixed* body output the +recorded rejected commitment is a determined value, and a continuation adversary could read the +random oracle at exactly that value, so the per-output charge is not `≤ ε`. The `ε` arises only by +averaging each rejected commitment over the fresh `ids.commit pk sk` draw +(`tsum_probOutput_commit_mul_count_le`). + +The charge is sound because the recorded read list is *value-free*: the continuation's +reads answer via `roStep` on the real layer and never the drawn (rejected) values, and the rejected +commitments are write-only (never cached; only accepted commitments are, via `cacheQuery`). The +value-substitution lemma `deferredDrawRead_run_count_dl_invariant` makes this precise: the +continuation's expected `readlist.count w` is invariant under the start drawn list, so the read list +is independent of every rejected draw's *value*. Combined with the body's draws being independent of +*reach* (a position is reached iff the earlier attempts rejected, which is determined by the earlier +draws — the body tape factorization `evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody` exhibits +this), each rejected draw charges its continuation read-multiplicity at the full marginal +`Pr[· | Prod.fst <$> commit] ≤ ε` (drop the reject indicator `≤ 1` on the value-substituted, hence +fixed, read list — no rejection-conditioning skew). The body's single unconditional signing query +(`+1`) pays the full-marginal head charge, and the read list `⊥` the attempt count factors +`E[readlist.length · #attempts] = E[readlist.length] · E[#attempts]`. + +**Proof.** Unfold the sign step (`deferredDrawReadImpl … (Sum.inr msg)`), which maps each +signing-body output `alc` to the post-state with drawn list `s.drawn ++ alc.1.2` and signed list +`msg :: s.signed`. The continuation charge from the post-body state is bounded per `alc` by the +inductive hypothesis `ih`; its pre-existing drawn count splits via `List.count_append` into the +start drawn count (matched against the right-hand side) and the *body coincidence* +`E[Σ_{rc ∈ readlist} alc.1.2.count rc]`, which the bilinear count swap `sum_map_count_comm` recasts +as `E[Σ_{w ∈ alc.1.2} readlist.count w]` and `ghostSignDrawBody_continuation_charge` bounds by +`ε · (rl.length + qH) · E[#attempts + 1]`. The slack length factor recombines via the deterministic +prefix monotonicities `deferredDrawRead_run_drawn_prefix` / `deferredDrawRead_run_signed_prefix`: +the gap between the start slack and the post-body slack is exactly `alc.1.2.length + 1` (the body's +rejected draws plus the single signing query), which the body's `+1` term covers. The continuation +run's full mass (`deferredDrawRead_run_neverFail`) makes the constant-length factor `L₀` exact. -/ +theorem nontape_signStep_charge {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (_hp₀ : 0 ≤ p_abort) (_hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (_hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (msg : M) + (ob : ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg) → + OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (s : DeferredReadState M Commit Chal) + (hob : ∀ u, (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH) + (ih : ∀ (u : ((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) + (s' : DeferredReadState M Commit Chal), + (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH → + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob u)).run s'] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob u)).run s'] * + ((z.2.2.map (fun rc => s'.1.1.2.count rc)).sum : ℝ≥0∞)) + + ENNReal.ofReal ε * ((s'.2.length + qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob u)).run s'] * + (((z.2.1.1.2.length - s'.1.1.2.length) + + (z.2.1.1.1.2.length - s'.1.1.1.2.length) : ℕ) : ℝ≥0∞)) : + (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ ∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + ((Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.2.map (fun rc => s.1.1.2.count rc)).sum : ℝ≥0∞)) + + ENNReal.ofReal ε * ((s.2.length + qH : ℕ) : ℝ≥0∞) * + (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞))) := by + classical + set L₀ : ℝ≥0∞ := ENNReal.ofReal ε * ((s.2.length + qH : ℕ) : ℝ≥0∞) with hL₀ + -- Unfold the sign step: it maps each signing-body output `alc` to the post-state with drawn list + -- `s.drawn ++ alc.1.2` and signed list `msg :: s.signed`. Convert all three sign-step averages to + -- averages over the signing-body output `alc`. + have hLHS : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), + s.2)] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞) := + tsum_probOutput_map_mul _ _ _ + have hRHS1 : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + ((z.2.2.map (fun rc => s.1.1.2.count rc)).sum : ℝ≥0∞)) + = ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), + s.2)] * + ((z.2.2.map (fun rc => s.1.1.2.count rc)).sum : ℝ≥0∞) := + tsum_probOutput_map_mul _ _ _ + have hRHS2 : (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + L₀ * (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞))) + = L₀ * ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), + s.2)] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞) := by + rw [ENNReal.tsum_mul_left]; exact congrArg (L₀ * ·) (tsum_probOutput_map_mul _ _ _) + -- Rewrite all three sums to body averages; the RHS is `(pre-existing) + L₀ · (slack)`. + rw [hLHS] + rw [ENNReal.tsum_add] + conv_rhs => rw [show (∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + ENNReal.ofReal ε * ((s.2.length + qH : ℕ) : ℝ≥0∞) * + (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞))) + = ∑' x : (((unifSpec + (M × Commit →ₒ Chal)) + + (M →ₒ Option (Commit × Resp))).Range (Sum.inr msg)) × DeferredReadState M Commit Chal, + L₀ * (Pr[= x | (deferredDrawReadImpl ids M maxAttempts pk sk (Sum.inr msg)).run s] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob x.1)).run x.2] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞)) from + tsum_congr fun x => by rw [hL₀]] + rw [hRHS1, hRHS2] + -- Both sides are now body averages; bound the LHS per `alc` by the inductive hypothesis at the + -- post-body state, splitting the pre-existing drawn count and applying induction (1) to the body + -- coincidence and the slack length identities. + -- The body-coincidence charge `E_alc[E_z[Σ_{rc∈readlist} alc.1.2.count rc]]` is bounded by + -- induction (1) (after the bilinear count swap `sum_map_count_comm`). + have hbody : (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + ≤ L₀ * ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) := by + rw [hL₀] + exact ghostSignDrawBody_continuation_charge ids M maxAttempts qH ε hε pk sk hGuess msg ob + (fun u => hob u) (msg :: s.1.1.1.2) s.2 s.1.2 maxAttempts s.1.1.1.1 s.1.1.2 + -- The continuation runs never fail, so their mass is `1`. + have hcontMass : ∀ alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)]) = 1 := fun alc => + tsum_probOutput_eq_one' (deferredDrawRead_run_neverFail ids M maxAttempts pk sk (ob alc.1.1) _) + -- Per `alc`: split the pre-existing drawn count and rewrite the slack via the prefix lemmas. + -- The slack inner sum splits as the post-body inductive slack plus the body's `#attempts + 1`. + have hslack : ∀ (alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞)) + = (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + (((z.2.1.1.2.length - (s.1.1.2 ++ alc.1.2).length) + + (z.2.1.1.1.2.length - (msg :: s.1.1.1.2).length) : ℕ) : ℝ≥0∞)) + + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) := by + intro alc + rw [show ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) + = (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)]) * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) from by rw [hcontMass alc, one_mul]] + rw [← ENNReal.tsum_mul_right, ← ENNReal.tsum_add] + refine tsum_congr fun z => ?_ + by_cases hz : z ∈ support ((simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)) + · have hdr := deferredDrawRead_run_drawn_prefix ids M maxAttempts pk sk (ob alc.1.1) + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2) z hz + have hsg := deferredDrawRead_run_signed_prefix ids M maxAttempts pk sk (ob alc.1.1) + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2) z hz + simp only at hdr hsg + rw [← mul_add] + congr 1 + rw [← Nat.cast_add] + congr 1 + simp only [List.length_append, List.length_cons] at hdr hsg ⊢ + omega + · rw [probOutput_eq_zero_of_not_mem_support hz, zero_mul, zero_mul, zero_mul, add_zero] + -- Per `alc`: the inductive hypothesis at the post-body state, with the pre-existing drawn count + -- split into the start drawn count and the body coincidence (the bilinear count swap). + have h_alc : ∀ (alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache), + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + ((z.2.2.map (fun rc => s.1.1.2.count rc)).sum : ℝ≥0∞)) + + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + ((alc.1.2.map (fun w => z.2.2.count w)).sum : ℝ≥0∞)) + + L₀ * ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) (ob alc.1.1)).run + ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2)] * + (((z.2.1.1.2.length - (s.1.1.2 ++ alc.1.2).length) + + (z.2.1.1.1.2.length - (msg :: s.1.1.1.2).length) : ℕ) : ℝ≥0∞) := by + intro alc + have hih := ih alc.1.1 ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), s.2) + (hob alc.1.1) + -- The post-state read list is `s.2`, so the read-length factor is `L₀`. + simp only [hL₀.symm] at hih ⊢ + refine le_trans hih (le_of_eq ?_) + congr 1 + -- Pre-existing drawn count `(s.drawn ++ alc.1.2).count` splits into `s.drawn.count` plus the + -- body coincidence (via the bilinear count swap). + rw [← ENNReal.tsum_add] + refine tsum_congr fun z => ?_ + rw [← mul_add] + congr 1 + rw [← Nat.cast_add] + congr 1 + rw [show (z.2.2.map (fun rc => (s.1.1.2 ++ alc.1.2).count rc)) + = z.2.2.map (fun rc => s.1.1.2.count rc + alc.1.2.count rc) from + List.map_congr_left fun rc _ => by rw [List.count_append]] + rw [List.sum_map_add, sum_map_count_comm alc.1.2 z.2.2] + -- Assemble: sum the per-`alc` bound, split into pre-existing + body-coincidence + ih-slack, then + -- recombine the slack via `hslack` (the `#attempts + 1` gap) and the coincidence via `hbody`. + refine le_trans (ENNReal.tsum_le_tsum fun alc => mul_le_mul' le_rfl (h_alc alc)) ?_ + simp_rw [mul_add] + rw [ENNReal.tsum_add, ENNReal.tsum_add, add_assoc] + refine add_le_add le_rfl ?_ + -- The body coincidence plus the post-body inductive slack equal `L₀ · (the full RHS slack)`. + have hslackSum : + L₀ * ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ((alc.1.2.length + 1 : ℕ) : ℝ≥0∞) + + (∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + (L₀ * ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), + s.2)] * + (((z.2.1.1.2.length - (s.1.1.2 ++ alc.1.2).length) + + (z.2.1.1.1.2.length - (msg :: s.1.1.1.2).length) : ℕ) : ℝ≥0∞))) + = L₀ * ∑' alc : (Option (Commit × Resp) × List Commit) × (M × Commit →ₒ Chal).QueryCache, + Pr[= alc | (ghostSignDrawBody ids M pk sk msg maxAttempts).run s.1.1.1.1] * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) + (ob alc.1.1)).run ((((alc.2, msg :: s.1.1.1.2), s.1.1.2 ++ alc.1.2), s.1.2), + s.2)] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞) := by + rw [← ENNReal.tsum_mul_left, ← ENNReal.tsum_mul_left, ← ENNReal.tsum_add] + refine tsum_congr fun alc => ?_ + rw [hslack alc, mul_add, add_comm] + ring + rw [← hslackSum] + exact add_le_add hbody le_rfl + +omit [SampleableType Stmt] in +/-- **The general per-pair charge over the inline read-recording run (induction carrier).** For an +arbitrary start state `s`, the expected pair count `E[Σ_{rc ∈ readlist} drawnlist.count rc]` is at +most the *un-charged pre-existing* contribution `E[Σ_{rc ∈ readlist} s.drawnlist.count rc]` (the +start drawn list, which the adversary may target deterministically) plus `ε` times the expected +`readlist.length · #new-attempts`, where `#new-attempts` counts only the draws and signing queries +made *after* `s` (the new drawn-list and signed-list growth). The base instance (empty start drawn +list) has a zero pre-existing term, giving `readRecord_expected_pairs_nontape_le`. + +By induction on `oa`: +* **pure** — readlist and drawn list are the start ones; the pre-existing term *is* the pair count + and there are no new attempts (equality). +* **read** — the drawn and signed lists are unchanged, so the bound passes through the inductive + hypothesis (the bound never references the start *read* list, only the final one). +* **sign** — the body's fresh rejected draws extend the drawn list; the inductive hypothesis charges + them as part of the continuation's pre-existing term, which splits as the genuine pre-existing + term plus the body's contribution `E[Σ_{rc ∈ readlist} body-rejects.count rc]`, bounded by + `ε · E[readlist.length · #body-rejects]` via the body-charge `nontape_signStep_body_charge` (the + body's rejected values are independent of the value-free final read list); the residual `#new` + attempt slack (`+1` per query) is absorbed. -/ +theorem readRecord_expected_pairs_nontape_general {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (hQ : oa.IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH) + (s : DeferredReadState M Commit Chal) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + ((z.2.2.map (fun rc => s.1.1.2.count rc)).sum : ℝ≥0∞)) + + ENNReal.ofReal ε * ((s.2.length + qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run s] * + (((z.2.1.1.2.length - s.1.1.2.length) + + (z.2.1.1.1.2.length - s.1.1.1.2.length) : ℕ) : ℝ≥0∞) := by + classical + induction oa using OracleComp.inductionOn generalizing s qH with + | pure a => + simp only [simulateQ_pure, StateT.run_pure, tsum_probOutput_pure_mul] + simp only [add_zero, Nat.sub_eq_zero_of_le (le_refl _), Nat.cast_zero, mul_zero, add_zero] + exact le_refl _ + | query_bind t ob ih => + rw [OracleComp.isQueryBoundP_query_bind_iff] at hQ + obtain ⟨hQ1, hQ2⟩ := hQ + simp only [simulateQ_bind, simulateQ_query, OracleQuery.input_query, OracleQuery.cont_query, + id_map, StateT.run_bind] + rw [tsum_probOutput_bind_mul, tsum_probOutput_bind_mul, tsum_probOutput_bind_mul, + ← ENNReal.tsum_mul_left, ← ENNReal.tsum_add] + rcases t with (n | mc) | msg + · -- UNIFORM: the step is deterministic in the state (`x.2 = s`); factor per step output and + -- apply the inductive hypothesis directly (drawn / signed / read lists unchanged, budget + -- unchanged: uniform queries are not read queries). + have hQ2' : ∀ u, (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH := by + intro u; exact hQ2 u + refine ENNReal.tsum_le_tsum fun x => ?_ + by_cases hx : x ∈ support ((deferredDrawReadImpl ids M maxAttempts pk sk + (Sum.inl (Sum.inl n))).run s) + · have hxs : x ∈ support ((fun u => (u, s)) <$> + (HasQuery.toQueryImpl (spec := unifSpec) (m := ProbComp)) n) := hx + rw [support_map] at hxs + obtain ⟨u, _, rfl⟩ := hxs + beta_reduce + rw [mul_left_comm (ENNReal.ofReal ε * ((s.2.length + qH : ℕ) : ℝ≥0∞)), ← mul_add] + gcongr + exact ih u qH (hQ2' u) s + · rw [probOutput_eq_zero_of_not_mem_support hx]; simp + · -- READ: the post-state drawn / signed lists are unchanged; the read list grows by one and + -- the read budget decrements by one, so the constant `readlist.length + qH` is preserved. + have hpos : 0 < qH := by + rcases hQ1 with h | h + · exact absurd rfl h + · exact h + have hQ2' : ∀ cu : Chal × (M × Commit →ₒ Chal).QueryCache, + (ob cu.1).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) (qH - 1) := by + intro cu; exact hQ2 cu.1 + refine ENNReal.tsum_le_tsum fun x => ?_ + by_cases hx : x ∈ support ((deferredDrawReadImpl ids M maxAttempts pk sk + (Sum.inl (Sum.inr mc))).run s) + · have hxs : x ∈ support ((fun cu : Chal × (M × Commit →ₒ Chal).QueryCache => + (cu.1, ((((cu.2, s.1.1.1.2), s.1.1.2), s.1.2 || decide (mc.2 ∈ s.1.1.2)), + mc.2 :: s.2))) <$> roStep M s.1.1.1.1 mc) := hx + rw [support_map] at hxs + obtain ⟨cu, _, rfl⟩ := hxs + beta_reduce + have hconst : ((s.2.length + qH : ℕ) : ℝ≥0∞) + = (((mc.2 :: s.2).length + (qH - 1) : ℕ) : ℝ≥0∞) := by + simp only [List.length_cons]; congr 1; omega + rw [hconst, mul_left_comm (ENNReal.ofReal ε * (((mc.2 :: s.2).length + (qH - 1) : ℕ) : + ℝ≥0∞)), ← mul_add] + gcongr + exact ih cu.1 (qH - 1) (hQ2' cu) ((((cu.2, s.1.1.1.2), s.1.1.2), + s.1.2 || decide (mc.2 ∈ s.1.1.2)), mc.2 :: s.2) + · rw [probOutput_eq_zero_of_not_mem_support hx]; simp + · -- SIGN: the body's fresh rejected draws extend the drawn list; the body charge must keep + -- the body draws *averaged* (a fixed body output lets the adversary target the recorded + -- value), so the sum over body outputs is retained. The read budget is unchanged (signing + -- is not a read query), so the continuation's `readlist.length` is bounded by the same + -- constant `s.2.length + qH`. This is the value-free sign-step charge. + have hQ2' : ∀ u, (ob u).IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH := by + intro u; exact hQ2 u + exact nontape_signStep_charge ids M maxAttempts qH ε p_abort hp₀ hp hε pk sk hGuess + hAbort msg ob s hQ2' (fun u s' hQ' => ih u qH hQ' s') + +omit [SampleableType Stmt] in +/-- **The value-free per-pair charge over the *inline* (non-tape) read-recording run — the direct +characterization consumed by `readRecord_expected_coincidences_le`.** The expected pair count +`E[Σ_{rc ∈ readlist} drawnlist.count rc]` of the `deferredDrawReadImpl` run is at most `ε · qH` +times the expected attempt count. + +In this representation each rejected commitment is drawn *inline* at its signing step, so each fresh +draw sits in the independent-of-the-readlist position required by the atomic value-free charge +`tsum_probOutput_commit_mul_count_le`: the recorded reads answer from `roStep` on the real layer and +never the drawn (rejected) values, so the final read list is independent of every rejected draw. +The proof instantiates the inline-run induction `readRecord_expected_pairs_nontape_general` at the +empty-drawn-list start state, where the pre-existing term vanishes and the constant read-length +factor collapses to the read budget `qH`. + +The charge is against `#attempts := drawnlist.length + (signedlist.length − l.length)` +(= #rejects + #signing-queries), whose mean is `qSrem/(1-p)`; the `drawnlist.length`-only form is +unsound (it omits the accepting attempts' fresh draws). The start drawn list is empty +(no pre-existing draws the adversary could target deterministically). + +The same inequality over the front-loaded tape representation is +`readRecord_expected_pairs_tape_le`; see also `readRecord_expected_pairs_le`, which restates this +bound with the tape's signing-query budget `qSrem` in scope. Both are separate reusable +infrastructure and are not on the live path. -/ +theorem readRecord_expected_pairs_nontape_le {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (hQ : oa.IsQueryBoundP (· matches Sum.inl (Sum.inr _)) qH) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : + ℝ≥0∞) := by + -- Instantiate the general carrier at the empty-drawn-list start state: the pre-existing term + -- vanishes (`[].count _ = 0`), the constant read-length factor `s.2.length + qH` becomes `qH` + -- (empty start read list), and `#new-attempts` becomes the target `#attempts`. + have hgen := readRecord_expected_pairs_nontape_general ids M maxAttempts qH ε p_abort hp₀ hp hε + pk sk hGuess hAbort oa hQ ((((re, l), []), false), []) + simp only [List.count_nil, List.map_const', List.sum_replicate, smul_zero, Nat.sub_zero, + List.length_nil, Nat.cast_zero, mul_zero, tsum_zero, zero_add] at hgen + exact hgen + +omit [SampleableType Stmt] in +/-- **The value-free per-pair charge in the tape-factored representation.** Separate reusable +infrastructure: the same bound as `readRecord_expected_pairs_nontape_le`, stated over the +front-loaded run produced by `evalDist_deferredDrawRead_eq_drawList_tapeDrawRead`, in which the +read-recording run reads `drawList (ids.commit pk sk) (maxAttempts·qSrem) >>= fun tape => …` with +the draw tape sampled *upfront* as one independent block. It is not on the live path of +`readRecord_expected_coincidences_le`, which consumes the inline form directly. + +In this representation the recorded drawn list is a function of the tape (its rejected entries) +while the recorded read list is **value-free** (the reads answer from `roStep` on the real layer, +never the tape values), so the read list is manifestly independent of the tape values. The +proof transports each tape probability back to the corresponding inline `deferredDrawReadImpl` +probability through the fold equality and applies `readRecord_expected_pairs_nontape_le`. + +**The charge is against `#attempts`, not `drawnlist.length`.** The per-position reading of the bound +takes each *consumed* tape position (dropping the reject check): `drawnlist.count rc ≤ #consumed +positions k with `tape[k].1 = rc``, and for a fixed position `tape[k]` is a fresh raw +`Prod.fst <$> ids.commit` draw of mass `≤ ε` (`hGuess`), independent of the value-free `rc` and of +whether `k` is reached (reach depends only on *earlier* tape entries), giving +`≤ ε · readlist.length · #consumed`. The RHS therefore uses +`#attempts := drawnlist.length + (signedlist.length − l.length)` +(= #rejects + #signing-queries `≥` #consumed), whose mean is `qSrem/(1-p)` +(`deferredDrawRead_attemptKn_mean_le`). A `drawnlist.length`-form RHS would be false, since +charging all consumed positions exceeds the rejected-only count by the accepted positions, +`ε · E[#accepts]`. + +The structural fact underlying the tape reading is *functional*, not distributional: the +accept/reject decision of `tapeSignBody` on the head `(w, st)` is `ids.respond pk sk st c = none`, +which depends on the `PrvState` part `st` and the challenge `c` but **not on the `Commit` part +`w`**. So for any fixed state and challenge randomness, at a position the body *rejects*, replacing +`tape[k].1 = w` by any other `w'` leaves the output, the real cache, and (therefore, through the +value-free `roStep` read channel) the recorded read list unchanged — only the recorded drawn list +changes. The accept branch returns `(some (w, z), [])`, so `w` enters the output/signature there, +which is why the reject indicator excludes accepted positions; the reject branch records `w` +write-only into the drawn list. On the inline route this same value-substitution fact is +`deferredDrawRead_run_count_dl_invariant`. -/ +theorem readRecord_expected_pairs_tape_le {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (qSrem : ℕ) + (hQ : FiatShamir.signHashQueryBound M (S' := Option (Commit × Resp)) (oa := oa) qSrem qH) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | OracleComp.drawList (ids.commit pk sk) (maxAttempts * qSrem) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) oa).run + (((((re, l), []), false), []), tape)] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | OracleComp.drawList (ids.commit pk sk) (maxAttempts * qSrem) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) oa).run + (((((re, l), []), false), []), tape)] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : + ℝ≥0∞) := by + -- The content is the per-position value-free charge. The independence it rests on is + -- `readlist ⊥ (rejected tape position's VALUE)`: the tape→readlist channel runs only through + -- signatures (= ACCEPTED entries), so a rejected position's `Commit` value never enters any read + -- target or query answer (reads answer via `roStep` on the real layer). Front-loading the draws + -- (`evalDist_deferredDrawRead_eq_drawList_tapeDrawRead`) exhibits the draws as one independent + -- block but does not by itself supply that independence. + -- Transport BACK to the non-tape run via the fold equality: every tape probability equals the + -- corresponding non-tape `deferredDrawReadImpl` run probability. This makes the recorded draws + -- *inline-fresh* (drawn at each sign step) rather than front-loaded, which is the position in + -- which each rejected draw is independent of the (value-free) final read list, and lets + -- `readRecord_expected_pairs_nontape_le` discharge the goal. + have hfold := evalDist_deferredDrawRead_eq_drawList_tapeDrawRead ids M maxAttempts pk sk oa qSrem + hQ.1 ((((re, l), []), false), []) + have hpr : ∀ z : γ × DeferredReadState M Commit Chal, + Pr[= z | OracleComp.drawList (ids.commit pk sk) (maxAttempts * qSrem) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) oa).run + (((((re, l), []), false), []), tape)] = + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] := + fun z => by rw [probOutput_def, probOutput_def, ← hfold] + simp only [hpr] + exact readRecord_expected_pairs_nontape_le ids M maxAttempts qH ε p_abort hp₀ hp hε pk sk + hGuess hAbort oa hQ.2 re l + +omit [SampleableType Stmt] in +/-- **The value-free per-pair atom, stated with the signing-query budget `qSrem` in scope.** +Separate reusable infrastructure obtained by routing `readRecord_expected_pairs_nontape_le` through +the tape representation and back; it is not on the live path of +`readRecord_expected_coincidences_le`, which consumes the inline form directly. + +The expected pair count — the expected number of coinciding +`(recorded read-commit, recorded drawn commit)` pairs, `E[Σ_{rc ∈ readlist} drawnlist.count rc]` — +is at most `ε · qH` times the expected attempt count. The content is the per-pair value-free +independence: for every `(read slot, draw slot)` pair, `E[1[rc = d]] ≤ ε`, because +* each recorded drawn commit `d` is a fresh i.i.d. raw `Prod.fst <$> ids.commit pk sk` draw of mass + `≤ ε` (`hGuess`), recorded write-only on rejected attempts (the accept branch records `[]`); +* the recorded read-commit list is **value-free** — the reads answer from the real RO layer via + `roStep`, never the drawn values (`blindStepProj_map_ghostBlindImpl_indep` / + `ghostHybridImpl_proj_trans`), so the readlist is jointly independent of the drawn *values*. + +Summing the per-pair bound over the `readlist.length · drawnlist.length` pairs gives the claim. The +factoring `E[Σ_pairs 1[rc=d]] = Σ_pairs E[1[rc=d]]` cannot be read off a single step of the opaque +adversary `simulateQ (oa)` fold: a draw-before-read pair has its draw resolved before the later +read, so the read-step increment is deterministic in the pre-state and is not `≤ ε` at that step. +What supplies the bound instead is the global independence of the readlist law from the +drawn-value law, established on the inline route by the value-substitution invariant +`deferredDrawRead_run_count_dl_invariant` inside `nontape_signStep_charge`. + +**Tape factorization.** The same independence is exhibited representationally by the two halves of +the tape factorization: `evalDist_ghostSignDrawBody_eq_drawList_tapeSignBody` recasts one signing +body's inline attempt draws as consumption from a pre-drawn tape +(`𝒟[(ghostSignDrawBody … n).run re] = 𝒟[drawList (ids.commit pk sk) n >>= tapeSignBody … tape]`) +via the local i.i.d. resampling commute `evalDist_bind_comm_probComp`, and +`evalDist_deferredDrawRead_eq_drawList_tapeDrawRead` lifts that across the `simulateQ (oa)` fold, so +the per-query tape blocks of every interleaved signing query commute to the very front as a single +independent draw block of `maxAttempts · qSrem` commitments, past the adaptive read points. + +The surrounding reduction is `countP_mem_le_sum_count`, the deterministic readlist-length bound +`deferredDrawReadImpl_run_readlist_length_le`, the expected drawn-list length fold +`deferredDrawRead_run_expected_drawnlist_length_le`, and the final arithmetic. -/ +theorem readRecord_expected_pairs_le {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (qSrem : ℕ) + (hQ : FiatShamir.signHashQueryBound M (S' := Option (Commit × Resp)) (oa := oa) qSrem qH) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : + ℝ≥0∞) := by + classical + -- STEP C: transport both expectations through the fold-level tape factorization, so the recorded + -- draws become a function of the front tape and the value-free read list becomes independent of + -- the tape values. + have hfold := evalDist_deferredDrawRead_eq_drawList_tapeDrawRead ids M maxAttempts pk sk oa qSrem + hQ.1 ((((re, l), []), false), []) + have hpr : ∀ z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] = + Pr[= z | OracleComp.drawList (ids.commit pk sk) (maxAttempts * qSrem) >>= fun tape => + (fun p : γ × (DeferredReadState M Commit Chal × List (Commit × PrvState)) => + (p.1, p.2.1)) <$> + (simulateQ (tapeDrawReadImpl ids M maxAttempts pk sk) oa).run + (((((re, l), []), false), []), tape)] := + fun z => by rw [probOutput_def, probOutput_def, hfold] + simp only [hpr] + -- The per-pair charge in the tape-factored representation: with `drawnlist = f(tape)` (recorded + -- rejected tape entries) and `readlist` value-free (`roStep`), each `(read slot, draw slot)` pair + -- charges at most `ε` by `hGuess` (each tape entry is a fresh raw `Prod.fst <$> ids.commit` draw + -- of mass `≤ ε`, independent of the value-free read points). + exact readRecord_expected_pairs_tape_le ids M maxAttempts qH ε p_abort hp₀ hp hε pk sk + hGuess hAbort oa qSrem hQ re l + +omit [SampleableType Stmt] in +/-- **The expected-coincidence-count bound (the numeric core of the first-moment route).** +The read-recording run's expected coincidence count +`E[#{ rc ∈ readlist : rc ∈ drawnlist }]` — the first moment fed by the Markov step +`readRecord_pred_le_expected_coincidences` — is at most `qSrem · (qH+1) · ε / (1-p)`. + +This is the σ-free numeric form of the ghost-read charge (no front-loaded game, no all-miss +strategy `σ`): both the headline ghost-read bound and the `euf_cma` proof are charged through this +single numeric inequality. + +**The accounting (why this is TRUE and additive — no per-output skew).** The coincidence count is a +double sum `Σ_{rc ∈ readlist} Σ_{d ∈ drawnlist} 1[rc = d]`, hence purely additive; the +rejection-conditioning skew that broke every `Pr[bad]` / per-output route lives in +output-conditioning, never in a SUM. Bounding `E[count]` decomposes over (read, draw) pairs: +* each recorded drawn commit `d` is a fresh i.i.d. raw `Prod.fst <$> ids.commit pk sk` draw of mass + `≤ ε` (`hGuess`), recorded write-only on rejected attempts (the accept branch records `[]`); +* the recorded read-commit list is **value-free** — the reads answer from the real RO layer via + `roStep`, never the drawn values (`blindStepProj_map_ghostBlindImpl_indep` / + `ghostHybridImpl_proj_trans`), so the readlist is jointly independent of the drawn *values*; +* by that independence, for each pair `E[1[rc = d]] ≤ ε`, and there are `≤ (qH+1) · E[#attempts]` + pairs (`(qH+1)` reads by `hQ`, `E[#attempts] ≤ qSrem/(1-p)` by `deferredDraw_attemptKn_mean_le`), + giving `E[count] ≤ (qH+1) · ε · E[#attempts] ≤ qSrem · (qH+1) · ε / (1-p)`. + +**The reduction.** The bound reduces by elementary arithmetic to a +single value-free atom (see the scaffolding lemmas above): +* the coincidence count is dominated pointwise by the pair count + `Σ_{rc ∈ readlist} drawnlist.count rc` (`countP_mem_le_sum_count`); +* the recorded readlist has length `≤ qH` on the whole support — a *deterministic* bound from the + read-query budget (`deferredDrawReadImpl_run_readlist_length_le`, empty start readlist); +* the expected drawn-list length is `≤ qSrem · (1/(1-p))` (`deferredDrawRead_run_expected_…`, empty + start drawnlist); +* the genuine content is the **value-free per-pair atom** + `readRecord_expected_pairs_nontape_le`: + `E[Σ_{rc ∈ readlist} drawnlist.count rc] ≤ ε · E[readlist.length · drawnlist.length]`. + +**Where the probabilistic content sits.** The arithmetic after factoring the joint expectation is +linear and discharged here; the factoring itself — that each fresh draw is conditionally i.i.d. and +`⊥` the recorded readlist *through the opaque adversary `simulateQ (oa)` fold* — is supplied by +`readRecord_expected_pairs_nontape_le`. A threaded fold charges the *sign* steps directly (each +fresh draw `⊥` the *current* readlist, value-free, additive), but a *draw-before-read* pair has its +draw resolved before the later read, so the read-step increment `1[mc.2 ∈ drawnlist]` is +deterministic in the pre-state and is not `≤ ε` at that single step. What covers it is the global +independence of the readlist law from the drawn-value law, established as the value-substitution +invariant `deferredDrawRead_run_count_dl_invariant` carried through +`readRecord_expected_pairs_nontape_general` / `nontape_signStep_charge`. + +The start drawn list is empty (`ws₀ = []`): the bound is sound only with no pre-existing draws, +since the adversary's read points are value-free w.r.t. the run's fresh draws but can +deterministically target a fixed pre-existing commitment. The headline instance uses the empty +start. -/ +theorem readRecord_expected_coincidences_le {γ : Type} + (qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) + (oa : OracleComp ((unifSpec + (M × Commit →ₒ Chal)) + (M →ₒ Option (Commit × Resp))) γ) + (qSrem : ℕ) + (hQ : FiatShamir.signHashQueryBound M (S' := Option (Commit × Resp)) (oa := oa) qSrem qH) + (re : (M × Commit →ₒ Chal).QueryCache) (l : List M) : + (∑' z : γ × DeferredReadState M Commit Chal, + Pr[= z | (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run + ((((re, l), []), false), [])] * + (z.2.2.countP (fun rc => decide (rc ∈ z.2.1.1.2)) : ℝ≥0∞)) + ≤ ENNReal.ofReal ((qSrem : ℝ) * ((qH : ℝ) + 1) * ε / (1 - p_abort)) := by + classical + obtain ⟨hQS, hQH⟩ := hQ + set run : ProbComp (γ × DeferredReadState M Commit Chal) := + (simulateQ (deferredDrawReadImpl ids M maxAttempts pk sk) oa).run ((((re, l), []), false), []) + with hrun + -- Step 1+2: dominate the coincidence count pointwise by the pair count. + have hstep12 : + (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + (z.2.2.countP (fun rc => decide (rc ∈ z.2.1.1.2)) : ℝ≥0∞)) + ≤ ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞) := by + refine ENNReal.tsum_le_tsum fun z => ?_ + gcongr + exact_mod_cast countP_mem_le_sum_count z.2.2 z.2.1.1.2 + -- Step 3 (the atom): the expected pair count is `≤ ε · qH · E[#attempts]`, where the read-list + -- length is dominated *deterministically* by the read-query budget `qH` (the constant factor + -- threaded through the carrier), and `#attempts := drawnlist.length + (signedlist.length − + -- l.length)` (= #rejects + #queries). The `#attempts` (not `drawnlist.length = #rejects`) factor + -- is the sound charge: charging per consumed tape position (drop-reject) covers all reached + -- attempts, which dominates the rejected ones; its mean is the same `qSrem/(1-p)` as the drawn. + have hatom : + (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.2.map (fun rc => z.2.1.1.2.count rc)).sum : ℝ≥0∞)) + ≤ ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : + ℝ≥0∞) := by + rw [hrun] + exact readRecord_expected_pairs_nontape_le ids M maxAttempts qH ε p_abort hp₀ hp hε pk sk + hGuess hAbort oa hQH re l + -- Step 5: `E[#attempts] ≤ qSrem · (1/(1-p))` (empty start drawnlist; + -- `deferredDrawRead_attemptKn_mean_le`). + have hdraw : + (∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞)) + ≤ (qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort)) := by + rw [hrun] + exact deferredDrawRead_attemptKn_mean_le ids M maxAttempts pk sk hp₀ hp hAbort + oa qSrem hQS re l + -- Assemble the chain and convert to the target `ofReal` form. The exposed `(qH+1)` constant is + -- the (loose) weakening of the deterministic read-length bound `qH`. + refine le_trans hstep12 (le_trans hatom ?_) + have hchain : ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) + ≤ ENNReal.ofReal ε * ((qH : ℝ≥0∞) + 1) * (qSrem : ℝ≥0∞) * + ENNReal.ofReal (1 / (1 - p_abort)) := by + calc ENNReal.ofReal ε * ((qH : ℕ) : ℝ≥0∞) * + ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) + ≤ ENNReal.ofReal ε * ((qH : ℝ≥0∞) + 1) * + ∑' z : γ × DeferredReadState M Commit Chal, Pr[= z | run] * + ((z.2.1.1.2.length + (z.2.1.1.1.2.length - l.length) : ℕ) : ℝ≥0∞) := by + gcongr + · exact le_self_add + _ ≤ ENNReal.ofReal ε * ((qH : ℝ≥0∞) + 1) * + ((qSrem : ℝ≥0∞) * ENNReal.ofReal (1 / (1 - p_abort))) := by + rw [mul_assoc, mul_assoc] + gcongr + _ = ENNReal.ofReal ε * ((qH : ℝ≥0∞) + 1) * (qSrem : ℝ≥0∞) * + ENNReal.ofReal (1 / (1 - p_abort)) := by ring + refine le_trans hchain (le_of_eq ?_) + have h1p : (0 : ℝ) < 1 - p_abort := by linarith + rw [show ((qH : ℝ≥0∞) + 1) = ENNReal.ofReal ((qH : ℝ) + 1) by + rw [ENNReal.ofReal_add (by positivity) (by norm_num)]; simp, + show ((qSrem : ℝ≥0∞)) = ENNReal.ofReal (qSrem : ℝ) by simp] + rw [← ENNReal.ofReal_mul hε, ← ENNReal.ofReal_mul (by positivity), + ← ENNReal.ofReal_mul (by positivity)] + congr 1 + field_simp + +omit [SampleableType Stmt] in +/-- **Ghost-blind ghost-read bound** (the sound headline target). The ghost-blind run's +adversarial-read bad mass is at most `qS·(qH+1)·ε/(1-p)`, via the first-moment route: the eager bad +mass is reduced to the deferred-draw run (`ghostBlind_bad_le_deferredDraw`), then to the +read-recording final-state read-hit predicate (`deferredDraw_bad_le_readRecord`), then to the +expected coincidence count by the Markov step (`readRecord_pred_le_expected_coincidences`), +which is finally charged by the numeric value-free bound `readRecord_expected_coincidences_le`. +Chaining with `probEvent_ghostHybridImpl_bad_le_ghostBlind` discharges the eager form +(`probEvent_ghostRead_bad_le`). -/ +theorem probEvent_ghostBlindImpl_bad_le + (qS qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostBlindImpl ids M maxAttempts pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ ENNReal.ofReal (qS * ((qH : ℝ) + 1) * ε / (1 - p_abort)) := by + -- The first-moment route: reduce the eager bad mass to the deferred-draw run, then to the + -- read-recording final-state predicate, then to the expected coincidence count (the Markov + -- step), and charge that count by the numeric value-free bound. + refine le_trans (ghostBlind_bad_le_deferredDraw ids M maxAttempts pk sk (adv.main pk) + ((((∅, ∅), []), false) : GhostState M Commit Chal) + ((((∅, []), []), false) : DeferredState M Commit Chal) + ⟨rfl, rfl, fun mc h => absurd rfl h, by simp⟩) ?_ + refine le_trans (deferredDraw_bad_le_readRecord ids M maxAttempts pk sk (adv.main pk) + ((((∅, []), []), false) : DeferredState M Commit Chal) + (((((∅, []), []), false), []) : DeferredReadState M Commit Chal) + ⟨rfl, fun h => absurd h (by simp)⟩) ?_ + refine le_trans (readRecord_pred_le_expected_coincidences ids M maxAttempts pk sk (adv.main pk) + (((((∅, []), []), false), []) : DeferredReadState M Commit Chal)) ?_ + exact readRecord_expected_coincidences_le ids M maxAttempts qH ε p_abort hp₀ hp hε pk sk + hGuess hAbort (adv.main pk) qS (hQ pk) ∅ [] + +omit [SampleableType Stmt] in +/-- **Ghost-read collision bound** for the Prog → Trans hop: the probability that the +adversary ever queries the random oracle at a ghost point (a rejected signing attempt's +programmed point) is at most `qS·(qH+1)·ε/(1-p)`. + +Probabilistic content (deferred sampling): a rejected attempt's commitment `w` enters +the ghost layer with the joint law of `(w, c)` conditioned on rejection, and influences +the run only through the ghost-domain membership tests of later adversarial queries. +Per (rejected attempt `j`, adversarial query `k`) pair, the conditional independence of +the post-rejection run from `w` given the rejection event yields +`Pr[query k hits attempt j] ≤ Pr[attempt j runs] · ε` (the `1/Pr[reject]` skew of the +conditioned commitment law cancels against the rejection probability of the attempt). +Summing the expected number of attempts (`≤ 1/(1-p)` per signing query by `hAbort`) +against the `qH` adversarial queries (`hQ`) gives the bound; the budget `qH + 1` leaves +one unit of slack for a verification read, which the freshness check already rules out +(see `ghostHybridImpl_preserves_signed_inv`). + +The abort probability is assumed to lie in `[0, 1)` (`hp₀`, `hp`), which is what makes the +geometric attempt factor `1/(1 - p_abort)` well defined and at least `1`; `hε : 0 ≤ ε` matches +the per-attempt guessing bound `hGuess`, and `hQ` pins the query budgets `(qS, qH)` of +`adv.main pk`. The proof reduces the eager ghost-read bad mass to the ghost-blind run +(`probEvent_ghostHybridImpl_bad_le_ghostBlind`, identical until bad) and closes it with the +first-moment bound `probEvent_ghostBlindImpl_bad_le`. -/ +lemma probEvent_ghostRead_bad_le + (qS qH : ℕ) (ε p_abort : ℝ) (hp₀ : 0 ≤ p_abort) (hp : p_abort < 1) (hε : 0 ≤ ε) + (hQ : ∀ pk, FiatShamir.signHashQueryBound M + (S' := Option (Commit × Resp)) (oa := adv.main pk) qS qH) + (pk : Stmt) (sk : Wit) + (hGuess : ∀ cm : Commit, + Pr[= cm | Prod.fst <$> ids.commit pk sk] ≤ ENNReal.ofReal ε) + (hAbort : Pr[= none | ids.honestExecution pk sk] ≤ ENNReal.ofReal p_abort) : + Pr[fun z : (M × Option (Commit × Resp)) × GhostState M Commit Chal => z.2.2 = true | + (simulateQ (ghostHybridImpl ids M maxAttempts true pk sk) (adv.main pk)).run + ((((∅, ∅), []) : + ((M × Commit →ₒ Chal).QueryCache × (M × Commit →ₒ Chal).QueryCache) × + List M), false)] + ≤ ENNReal.ofReal (qS * (qH + 1) * ε / (1 - p_abort)) := by + -- M1 reduces the eager ghost-read bad mass to the ghost-blind run's bad mass + -- (`probEvent_ghostHybridImpl_bad_le_ghostBlind`, identical until bad), and the ghost-blind + -- bound `probEvent_ghostBlindImpl_bad_le` (the first-moment route) closes it at + -- `qS·(qH+1)·ε/(1-p)`, with the numeric charge supplied by + -- `readRecord_expected_coincidences_le`. + refine (probEvent_ghostHybridImpl_bad_le_ghostBlind ids hr M maxAttempts adv pk sk).trans ?_ + refine le_trans (probEvent_ghostBlindImpl_bad_le ids hr M maxAttempts adv qS qH ε p_abort + hp₀ hp hε hQ pk sk hGuess hAbort) (le_of_eq ?_) + norm_cast + +end scaffold + +end EUF_CMA + +end FiatShamirWithAbort