|
| 1 | +--- |
| 2 | +source_branch: master |
| 3 | +source_path: src/Ledger/Dijkstra/Specification/Certs/Properties/ApplyWithdrawalsPoV.lagda.md |
| 4 | +--- |
| 5 | + |
| 6 | + |
| 7 | +# `applyWithdrawals` Preservation of Value {#sec:apply-withdrawals-pov} |
| 8 | + |
| 9 | +This module proves that `applyWithdrawals` decreases the total rewards balance |
| 10 | +by exactly the sum of the withdrawal amounts. This is the key new lemma |
| 11 | +for the Dijkstra (CIP-159) CERTS preservation-of-value proof. |
| 12 | + |
| 13 | +## Proof Strategy |
| 14 | + |
| 15 | +`applyWithdrawals` is defined as a `foldl` over the list representation of the |
| 16 | +withdrawal map. The proof proceeds by induction on this list, with a single-step |
| 17 | +lemma showing that each `applyOne` step decreases `getCoin` by exactly the |
| 18 | +withdrawal amount. |
| 19 | + |
| 20 | +The single-step argument decomposes the accumulator map `acc` into: |
| 21 | +`acc ≡ᵉ (acc ∣ ❴ c ❵ ᶜ) ∪ˡ (acc ∣ ❴ c ❵)` |
| 22 | +where `c = stake addr`. When `lookupᵐ? acc c ≡ just bal` and `amt ≤ bal`: |
| 23 | +`getCoin acc = getCoin (acc ∣ ❴ c ❵ ᶜ) + bal`, by decomposition; |
| 24 | +`getCoin (applyOne acc (addr , amt))` = `getCoin (❴ c , bal ∸ amt ❵ ∪ˡ (acc ∣ ❴ c ❵ ᶜ))` |
| 25 | += `(bal ∸ amt) + getCoin (acc ∣ ❴ c ❵ ᶜ)`, by disjoint union. |
| 26 | + |
| 27 | +So the decrease is `bal - (bal ∸ amt) = amt` (since `amt ≤ bal`). |
| 28 | + |
| 29 | +For the fold induction, the invariant is maintained because: |
| 30 | +- Each credential is targeted at most once (by injectivity of `stake` on `dom wdrls`, |
| 31 | + which follows from the `NetworkId` constraint). |
| 32 | +- `applyOne` preserves domain membership (it replaces entries, never removes them). |
| 33 | +- Therefore, remaining entries still have their credentials registered and their |
| 34 | + amounts bounded by the (unchanged) balances. |
| 35 | + |
| 36 | +<!-- |
| 37 | +```agda |
| 38 | +{-# OPTIONS --safe #-} |
| 39 | +
|
| 40 | +open import Ledger.Dijkstra.Specification.Gov.Base using (GovStructure) |
| 41 | +
|
| 42 | +module Ledger.Dijkstra.Specification.Certs.Properties.ApplyWithdrawalsPoV |
| 43 | + (gs : GovStructure) (open GovStructure gs) where |
| 44 | +
|
| 45 | +open import Ledger.Dijkstra.Specification.Certs gs |
| 46 | +open import Ledger.Dijkstra.Specification.Gov.Actions gs hiding (yes; no) |
| 47 | +open import Ledger.Prelude |
| 48 | +open import Axiom.Set.Properties th |
| 49 | +open import Data.Nat.Properties |
| 50 | + using ( +-0-monoid; +-identityʳ; +-identityˡ; +-comm; +-assoc |
| 51 | + ; m∸n+n≡m ) |
| 52 | +open import Relation.Binary using (IsEquivalence) |
| 53 | +
|
| 54 | +open RewardAddress |
| 55 | +
|
| 56 | +private variable |
| 57 | + A : Type |
| 58 | +
|
| 59 | +instance |
| 60 | + _ = +-0-monoid |
| 61 | +``` |
| 62 | +--> |
| 63 | + |
| 64 | +## Module parameters |
| 65 | + |
| 66 | +We parameterize over the standard finite-map sum lemmas (same pattern as Conway). |
| 67 | + |
| 68 | +<!-- |
| 69 | +```agda |
| 70 | +module ApplyWithdrawals-PoV |
| 71 | + ( indexedSumᵛ'-∪ : {A : Type} ⦃ _ : DecEq A ⦄ (m m' : A ⇀ Coin) |
| 72 | + → disjoint (dom m) (dom m') |
| 73 | + → getCoin (m ∪ˡ m') ≡ getCoin m + getCoin m' ) |
| 74 | + ( getCoin-cong : {A : Type} ⦃ _ : DecEq A ⦄ (s : A ⇀ Coin) (s' : ℙ (A × Coin)) |
| 75 | + → s ˢ ≡ᵉ s' → indexedSum' proj₂ (s ˢ) ≡ indexedSum' proj₂ s' ) |
| 76 | + where |
| 77 | + open ≡-Reasoning |
| 78 | + open Equivalence |
| 79 | + module ≡ᵉ = IsEquivalence (≡ᵉ-isEquivalence {Credential × Coin}) |
| 80 | +``` |
| 81 | +--> |
| 82 | + |
| 83 | +## Single-step lemma: `applyOne` decreases `getCoin` by `amt` |
| 84 | + |
| 85 | +When `stake addr ∈ dom acc` and `amt ≤ bal` (where `bal` is the current balance), |
| 86 | +applying a single withdrawal decreases the total by exactly `amt`. |
| 87 | + |
| 88 | +```agda |
| 89 | + applyOne-pov : |
| 90 | + (acc : Rewards) (addr : RewardAddress) (amt bal : Coin) |
| 91 | + → lookupᵐ? acc (stake addr) ≡ just bal |
| 92 | + → amt ≤ bal |
| 93 | + → getCoin acc ≡ getCoin (❴ stake addr , bal ∸ amt ❵ ∪ˡ (acc ∣ ❴ stake addr ❵ ᶜ)) + amt |
| 94 | +``` |
| 95 | + |
| 96 | +<!-- |
| 97 | +```agda |
| 98 | + applyOne-pov acc addr amt bal lookup-eq amt≤bal = {!!} |
| 99 | + -- Proof sketch: |
| 100 | + -- |
| 101 | + -- getCoin acc |
| 102 | + -- ≡ getCoin (acc ∣ ❴ c ❵ ᶜ) + getCoin (acc ∣ ❴ c ❵) |
| 103 | + -- -- by: acc ≡ᵉ (acc ∣ ❴ c ❵ ᶜ) ∪ (acc ∣ ❴ c ❵), then indexedSumᵛ'-∪ |
| 104 | + -- ≡ getCoin (acc ∣ ❴ c ❵ ᶜ) + bal |
| 105 | + -- -- by: getCoin-cong on (acc ∣ ❴ c ❵) ≡ᵉ ❴ c , bal ❵ (from lookup-eq) |
| 106 | + -- ≡ getCoin (acc ∣ ❴ c ❵ ᶜ) + (bal ∸ amt + amt) |
| 107 | + -- -- by: m∸n+n≡m amt≤bal |
| 108 | + -- ≡ (bal ∸ amt) + getCoin (acc ∣ ❴ c ❵ ᶜ) + amt |
| 109 | + -- -- by: +-comm, +-assoc |
| 110 | + -- ≡ getCoin (❴ c , bal ∸ amt ❵ ∪ˡ (acc ∣ ❴ c ❵ ᶜ)) + amt |
| 111 | + -- -- by: indexedSumᵛ'-∪ (disjoint since c ∉ dom (acc ∣ ❴ c ❵ ᶜ)) |
| 112 | + -- where c = stake addr |
| 113 | +``` |
| 114 | +--> |
| 115 | + |
| 116 | +## Fold invariant |
| 117 | + |
| 118 | +The fold invariant tracks three properties through the induction: |
| 119 | + |
| 120 | +1. All remaining withdrawal credentials are in the current accumulator's domain. |
| 121 | +2. All remaining withdrawal amounts are bounded by the current balances. |
| 122 | +3. Each credential appears at most once in the remaining list (NoDup on credentials). |
| 123 | + |
| 124 | +<!-- |
| 125 | +```agda |
| 126 | + -- The fold invariant for the induction. |
| 127 | + -- |
| 128 | + -- After processing some prefix of withdrawals, the remaining suffix still |
| 129 | + -- has all its credentials registered in the accumulator, with amounts bounded |
| 130 | + -- by current (possibly reduced) balances. |
| 131 | + -- |
| 132 | + -- The NoDup condition ensures each credential is targeted at most once, |
| 133 | + -- which is critical: applyOne replaces (not removes) the entry, so other |
| 134 | + -- credentials' balances are unchanged, but the same credential's balance |
| 135 | + -- IS reduced. NoDup guarantees we never revisit a reduced balance. |
| 136 | + -- |
| 137 | + -- NoDup on (mapˢ (stake ∘ proj₁) entries) follows from injectivity of |
| 138 | + -- `stake` on `dom wdrls`, which follows from the NetworkId constraint. |
| 139 | +``` |
| 140 | +--> |
| 141 | + |
| 142 | +## Main lemma: fold over the full list |
| 143 | + |
| 144 | +```agda |
| 145 | + applyOne : Rewards → RewardAddress × Coin → Rewards |
| 146 | + applyOne ac (addr , amt) = |
| 147 | + case lookupᵐ? ac (stake addr) of λ where |
| 148 | + (just bal) → ❴ stake addr , bal ∸ amt ❵ ∪ˡ (ac ∣ ❴ stake addr ❵ ᶜ) |
| 149 | + nothing → ac |
| 150 | +
|
| 151 | + foldl-applyOne-pov : |
| 152 | + (acc : Rewards) (entries : List (RewardAddress × Coin)) |
| 153 | + → (∀ {addr amt} → (addr , amt) ∈ˡ entries |
| 154 | + → stake addr ∈ dom acc |
| 155 | + × amt ≤ maybe id 0 (lookupᵐ? acc (stake addr))) |
| 156 | + -- → NoDup (map (stake ∘ proj₁) entries) -- needed for invariant preservation |
| 157 | + → getCoin acc ≡ getCoin (foldl applyOne acc entries) + sum (map proj₂ entries) |
| 158 | +``` |
| 159 | + |
| 160 | +<!-- |
| 161 | +```agda |
| 162 | + foldl-applyOne-pov = {!!} |
| 163 | + -- Proof by induction on entries: |
| 164 | + -- |
| 165 | + -- Base case (entries = []): |
| 166 | + -- getCoin acc ≡ getCoin acc + 0 -- by +-identityʳ |
| 167 | + -- |
| 168 | + -- Step case (entries = (addr , amt) ∷ rest): |
| 169 | + -- Let acc' = applyOne acc (addr , amt). |
| 170 | + -- By the invariant, stake addr ∈ dom acc and amt ≤ bal. |
| 171 | + -- So lookupᵐ? acc (stake addr) ≡ just bal for some bal. |
| 172 | + -- |
| 173 | + -- (1) applyOne-pov gives: |
| 174 | + -- getCoin acc ≡ getCoin acc' + amt |
| 175 | + -- |
| 176 | + -- (2) For the IH, we need the invariant for (acc', rest). |
| 177 | + -- - Domain: applyOne replaces the entry for `stake addr`, |
| 178 | + -- so all credentials remain in dom acc'. For credentials |
| 179 | + -- ≠ stake addr, the complement restriction preserves them. |
| 180 | + -- - Bounds: Since NoDup ensures `stake addr` does not appear |
| 181 | + -- again in rest, remaining entries target different credentials |
| 182 | + -- whose balances are unchanged by applyOne. |
| 183 | + -- |
| 184 | + -- (3) IH on (acc', rest) gives: |
| 185 | + -- getCoin acc' ≡ getCoin (foldl applyOne acc' rest) + sum (map proj₂ rest) |
| 186 | + -- |
| 187 | + -- (4) Combining (1) and (3): |
| 188 | + -- getCoin acc |
| 189 | + -- ≡ getCoin acc' + amt |
| 190 | + -- ≡ getCoin (foldl applyOne acc' rest) + sum (map proj₂ rest) + amt |
| 191 | + -- ≡ getCoin (foldl applyOne acc ((addr,amt) ∷ rest)) + (amt + sum (map proj₂ rest)) |
| 192 | + -- ≡ getCoin (foldl applyOne acc ((addr,amt) ∷ rest)) + sum (map proj₂ ((addr,amt) ∷ rest)) |
| 193 | +``` |
| 194 | +--> |
| 195 | + |
| 196 | +## Top-level lemma |
| 197 | + |
| 198 | +This is the form needed by `PRE-CERT-pov`. |
| 199 | + |
| 200 | +```agda |
| 201 | + applyWithdrawals-pov : |
| 202 | + (wdrls : Withdrawals) (rwds : Rewards) |
| 203 | + → mapˢ stake (dom wdrls) ⊆ dom rwds |
| 204 | + → (∀[ (addr , amt) ∈ wdrls ˢ ] amt ≤ maybe id 0 (lookupᵐ? rwds (stake addr))) |
| 205 | + → getCoin rwds ≡ getCoin (applyWithdrawals wdrls rwds) + getCoin wdrls |
| 206 | +``` |
| 207 | + |
| 208 | +<!-- |
| 209 | +```agda |
| 210 | + applyWithdrawals-pov = {!!} |
| 211 | + -- Proof: |
| 212 | + -- |
| 213 | + -- applyWithdrawals wdrls rwds = foldl applyOne rwds (setToList (wdrls ˢ)) |
| 214 | + -- |
| 215 | + -- Apply foldl-applyOne-pov with acc = rwds, entries = setToList (wdrls ˢ). |
| 216 | + -- |
| 217 | + -- The preconditions of foldl-applyOne-pov follow from: |
| 218 | + -- - Domain membership: from `mapˢ stake (dom wdrls) ⊆ dom rwds` |
| 219 | + -- - Amount bounds: from the second precondition |
| 220 | + -- - NoDup: from injectivity of `stake` on `dom wdrls` |
| 221 | + -- (which follows from the NetworkId constraint, established in PRE-CERT-pov) |
| 222 | + -- |
| 223 | + -- The remaining step is to relate: |
| 224 | + -- sum (map proj₂ (setToList (wdrls ˢ))) ≡ getCoin wdrls |
| 225 | + -- |
| 226 | + -- This holds because getCoin for a finite map is defined as |
| 227 | + -- indexedSumᵛ' which sums over the set representation, and |
| 228 | + -- summing proj₂ over the list representation of the same set |
| 229 | + -- gives the same result (by properties of setToList/indexedSum). |
| 230 | +``` |
| 231 | +--> |
| 232 | + |
| 233 | +## Supporting lemmas (to be proved) |
| 234 | + |
| 235 | +The following auxiliary properties are needed but not yet proved. |
| 236 | +They are standard finite-map facts independent of CIP-159. |
| 237 | + |
| 238 | +```agda |
| 239 | + -- applyOne preserves domain membership for other credentials. |
| 240 | + Claim-applyOne-dom-preserve : |
| 241 | + ∀ (acc : Rewards) (addr : RewardAddress) (amt : Coin) (c : Credential) |
| 242 | + → c ∈ dom acc → c ≢ stake addr |
| 243 | + → c ∈ dom (case lookupᵐ? acc (stake addr) of λ where |
| 244 | + (just bal) → ❴ stake addr , bal ∸ amt ❵ ∪ˡ (acc ∣ ❴ stake addr ❵ ᶜ) |
| 245 | + nothing → acc) |
| 246 | + Claim-applyOne-dom-preserve = {!!} |
| 247 | +
|
| 248 | + -- applyOne preserves balance for other credentials. |
| 249 | + Claim-applyOne-balance-preserve : |
| 250 | + ∀ (acc : Rewards) (addr : RewardAddress) (amt : Coin) (c : Credential) |
| 251 | + → c ≢ stake addr |
| 252 | + → lookupᵐ? (case lookupᵐ? acc (stake addr) of λ where |
| 253 | + (just bal) → ❴ stake addr , bal ∸ amt ❵ ∪ˡ (acc ∣ ❴ stake addr ❵ ᶜ) |
| 254 | + nothing → acc) c |
| 255 | + ≡ lookupᵐ? acc c |
| 256 | + Claim-applyOne-balance-preserve = {!!} |
| 257 | +``` |
0 commit comments