mirror of
https://github.com/saymrwulf/verifying-crypto-with-lean.git
synced 2026-09-04 20:03:41 +00:00
- pen-and-paper worked examples in all 12 chapters, using the REAL constants throughout: 2^-64 waiting-time arithmetic, headroom budgets, hand type-checking, rfl traces, full goal-state boards, the column-sum audit at 2^54, inverting 19 mod p via Euclid, the x19 fold at real weights, denoting p itself (telescope), the 16p audit (8 fails by 151), the 254+11 inversion-chain bookkeeping, the substitution test, sizing the 28-vs-1000 extraction, cofactor/torsion arithmetic, and the full Bernstein-Lange completeness derivation - CORRECTNESS FIX: ch7 asserted a false factorization of p-1; replaced with the computationally verified p-1 = 2^2 * 3 * 65147 * Q (Q 71-digit prime), witness w=2 verified for all four Pratt conditions - every chapter's exercises now followed immediately by 'Solutions and pathways' (pathway first, then answer), incl. new exercises - NEW Interlude: a complete two-clause verification done entirely by hand, then mapped line-by-line onto the compiled Lean proof - NEW appendices: A pen-and-paper toolkit (8 recipe cards + drills + answers), B guided walkthroughs of every exercise-file hole, C tour of the real repositories; plus glossary, instructor notes, 13-week plan - preamble: worked-example box, solution macros, math-safe inline code Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
391 lines
17 KiB
TeX
391 lines
17 KiB
TeX
\chapter{Meet Lean: A Language Where Programs and Proofs Live Together}
|
|
\label{ch:lean}
|
|
|
|
\section{First contact}
|
|
|
|
Lean~4 is two things wearing one syntax: a programming language (fast,
|
|
functional, compiled) and a proof assistant. You will learn both faces, but we
|
|
start with the friendlier one. Open a new file \code{Scratch.lean} and type:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
#eval 1 + 1 -- 2
|
|
#eval 2 ^ 255 - 19 -- a 77-digit number, instantly
|
|
#eval "hello".length -- 5
|
|
\end{lstlisting}
|
|
|
|
\lean{\#eval} runs an expression and prints the result right in your editor.
|
|
Notice the second line: Lean's natural numbers are \emph{arbitrary precision}
|
|
by default. The number $2^{255}-19$ --- which will follow us through the whole
|
|
book --- is a perfectly ordinary value here, not an overflow.
|
|
|
|
\lean{\#check} asks a different question: not ``what is the value?'' but
|
|
``what is the \emph{type}?''
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
#check 1 + 1 -- 1 + 1 : Nat
|
|
#check "hello" -- "hello" : String
|
|
#check (1 : Int) - 5 -- Int
|
|
\end{lstlisting}
|
|
|
|
\begin{bigidea}
|
|
In Lean, \textbf{every expression has a type}, and the type checker verifies
|
|
every file before anything runs. This obsession with types is not
|
|
bureaucracy --- in Chapter~\ref{ch:pat} it will turn out to be the entire
|
|
mechanism by which proofs work. Learn to read \code{e : T} as ``$e$ is of
|
|
type $T$'' --- or, with a squint we will justify later, ``$e$ is
|
|
\emph{evidence} for $T$.''
|
|
\end{bigidea}
|
|
|
|
\section{Definitions and functions}
|
|
|
|
New names are introduced with \lean{def}:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
def p : Nat := 2 ^ 255 - 19
|
|
|
|
def double (n : Nat) : Nat := 2 * n
|
|
|
|
def isEven (n : Nat) : Bool := n % 2 == 0
|
|
|
|
#eval double 21 -- 42
|
|
#eval isEven p -- false (p is odd, good: p is supposed to be prime!)
|
|
\end{lstlisting}
|
|
|
|
Three things to absorb from this snippet:
|
|
|
|
\begin{itemize}[leftmargin=1.4em]
|
|
\item Function application is written with a space: \lean{double 21}, not
|
|
\code{double(21)}. It looks strange for a week and then all other syntax
|
|
looks noisy forever after.
|
|
\item Every definition states its types: \lean{double} takes a \lean{Nat} and
|
|
returns a \lean{Nat}. Lean can often infer types, but in this book we write
|
|
them --- specifications are the whole game, and a type is a tiny
|
|
specification.
|
|
\item Definitions are \emph{immutable equations}, not instructions. There is
|
|
no ``assignment.'' \lean{p} \emph{is} $2^{255}-19$, forever.
|
|
\end{itemize}
|
|
|
|
Functions of several arguments simply take them in sequence, and functions
|
|
are values you can pass around:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
def addMul (a b c : Nat) : Nat := a + b * c
|
|
|
|
def twice (f : Nat -> Nat) (x : Nat) : Nat := f (f x)
|
|
|
|
#eval twice double 10 -- 40
|
|
\end{lstlisting}
|
|
|
|
The type of \lean{twice} is worth staring at:
|
|
\lean{(Nat -> Nat) -> Nat -> Nat}. Arrows associate to the right, and a
|
|
multi-argument function is really a chain of single-argument ones --- this is
|
|
called \emph{currying}. Nothing about it needs memorizing now; it will become
|
|
muscle memory.
|
|
|
|
\section{Inductive types: building data from nothing}
|
|
|
|
Where do types like \lean{Nat} and \lean{Bool} come from? They are not
|
|
built-in magic. They are \emph{inductive types} --- data types defined by
|
|
listing every way to construct a value. Here is \lean{Bool}, exactly as the
|
|
core library defines it:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
inductive Bool where
|
|
| false : Bool
|
|
| true : Bool
|
|
\end{lstlisting}
|
|
|
|
Read: ``a \lean{Bool} is either \lean{false} or \lean{true}, and there is no
|
|
other way to make one.'' That closed-world clause is what makes case analysis
|
|
--- and later, proofs by cases --- airtight.
|
|
|
|
Now the star of the show. The natural numbers, following Peano's 1889 idea:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
inductive Nat where
|
|
| zero : Nat -- 0 exists
|
|
| succ (n : Nat) : Nat -- every number has a successor
|
|
\end{lstlisting}
|
|
|
|
Every natural number is \lean{zero} wrapped in finitely many \lean{succ}s:
|
|
the number 3 \emph{is} \lean{succ (succ (succ zero))}. (Lean stores big
|
|
numbers efficiently under the hood, but \emph{reasons} about them through
|
|
this two-case skeleton.) Functions on inductive types are defined by
|
|
\emph{pattern matching} --- one equation per constructor:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
def add : Nat -> Nat -> Nat
|
|
| m, Nat.zero => m
|
|
| m, Nat.succ n => Nat.succ (add m n)
|
|
\end{lstlisting}
|
|
|
|
\begin{worked}{running the definition of addition by hand}
|
|
The two equations of \lean{add} are a complete calculator; let us be the
|
|
computer once, because every proof in Chapter~\ref{ch:tactics} secretly
|
|
replays this trace. Abbreviate \lean{Nat.succ} as $s$ and \lean{Nat.zero}
|
|
as $z$, so $3 = s(s(s(z)))$ and $2 = s(s(z))$. Compute
|
|
$\mathtt{add}\ 3\ 2$; at each step exactly one equation applies, decided by
|
|
the \emph{second} argument's outermost constructor:
|
|
\[
|
|
\begin{array}{lcl}
|
|
\mathtt{add}\ 3\ s(s(z)) &=& s(\mathtt{add}\ 3\ s(z))
|
|
\qquad\text{(second equation, } n = s(z))\\
|
|
&=& s(s(\mathtt{add}\ 3\ z))
|
|
\qquad\text{(second equation, } n = z)\\
|
|
&=& s(s(3))
|
|
\qquad\text{(first equation)}\\
|
|
&=& s(s(s(s(s(z))))) \;=\; 5 .
|
|
\end{array}
|
|
\]
|
|
Three observations to carry forward. (1) The recursion counts down the
|
|
\emph{second} argument only --- the first is inert. That asymmetry is why
|
|
\lean{n + 0 = n} will hold ``by computation'' while \lean{0 + n = n} will
|
|
not (Chapter~\ref{ch:pat} makes this precise). (2) Each step is justified
|
|
by \emph{matching a constructor} --- there is no arithmetic insight
|
|
anywhere, just pattern matching, which is why a small, dumb kernel can
|
|
check it. (3) The number of steps equals the second argument --- fine for
|
|
$2$, comedy for $2^{255}$; Lean therefore \emph{stores} numerals in
|
|
efficient binary and uses this unary skeleton only for \emph{reasoning}.
|
|
Keep the two roles separate in your head: fast representation for
|
|
computing, tiny inductive skeleton for proving.
|
|
\end{worked}
|
|
|
|
\begin{aha}
|
|
Look at what just happened. Addition --- the operation at the bottom of every
|
|
cryptosystem in this book --- is not an axiom or a CPU instruction here. It is
|
|
a \emph{two-line recursive program}, and every arithmetic fact we will ever
|
|
prove unwinds, ultimately, to these two equations. When Lean later claims
|
|
$a + b = b + a$ for \emph{all} numbers, it will be because the structure of
|
|
this definition forces it, not because anyone tested it.
|
|
\end{aha}
|
|
|
|
\begin{pitfall}
|
|
\lean{Nat} subtraction \emph{truncates}: \lean{\#eval (3 - 5 : Nat)} prints
|
|
\lean{0}, not $-2$, because natural numbers have nowhere to go below zero.
|
|
This single fact causes a large fraction of all beginner proof failures ---
|
|
an identity like $a - b + b = a$ is simply \emph{false} for \lean{Nat}. When
|
|
subtraction must mean subtraction, use \lean{Int}, or carry a hypothesis
|
|
$b \le a$. Real verification projects hit this constantly: machine arithmetic
|
|
wraps, truncates, and overflows, and the proofs must say so honestly.
|
|
\end{pitfall}
|
|
|
|
\begin{worked}{truncation meeting real cryptographic constants}
|
|
Do not file the truncation pitfall under ``beginner trivia''; run it against
|
|
the book's own constants. In the Ed25519 field code, subtraction of field
|
|
elements must compute $a - b$ \emph{as integers would}, but the limbs live
|
|
in \lean{Nat}-like unsigned words. Take the real situation: a fresh limb
|
|
$a_0 < 2^{51}$ and a lazily-grown limb $b_0 < 2^{54}$
|
|
(Chapter~\ref{ch:rust} explains how limbs grow). Then in \lean{Nat}:
|
|
\[
|
|
a_0 - b_0 \;=\; 0 \quad\text{whenever } b_0 > a_0
|
|
\qquad\text{--- the true difference is simply gone.}
|
|
\]
|
|
Concretely: $a_0 = 2^{51} - 19$ (the low limb of $p$ itself!) and
|
|
$b_0 = 2^{53}$: true difference $2^{51} - 19 - 2^{53} = -(3\cdot 2^{51} + 19)$,
|
|
truncated result $0$. Any downstream carry chain silently absorbs the $0$
|
|
and produces a well-formed, plausible, \emph{wrong} field element.
|
|
|
|
The production fix (verified in Chapter~\ref{ch:field}) is to compute
|
|
$a - b$ as $a + (16p - b)$: adding the multiple-of-$p$ constant $16p$ lifts
|
|
every limb difference above zero \emph{before} subtracting, and the extra
|
|
$16p$ vanishes modulo $p$. Check the crucial inequality yourself with the
|
|
real numbers: the largest subtrahend limb allowed by the bounds discipline
|
|
is $< 2^{54}$, and the smallest limb of the $16p$ constant is
|
|
$16 \cdot (2^{51} - 19) = 2^{55} - 304$, so every limb of $16p - b$ is at
|
|
least $2^{55} - 304 - (2^{54} - 1) = 2^{54} - 303 > 0$. No truncation, on
|
|
any input --- and note that the argument needed $16p$: the same computation
|
|
with $8p$ bottoms out at $2^{54} - 152 - (2^{54} - 1) = -151 < 0$, which
|
|
\emph{can} truncate. One design constant, justified by three lines of
|
|
pen-and-paper arithmetic you just verified.
|
|
\end{worked}
|
|
|
|
\begin{worked}{drawing the cost of a definition --- the \code{fib} call tree}
|
|
A definition's \emph{meaning} and its \emph{cost} are different objects,
|
|
and one drawing separates them forever. Take the naive recurrence
|
|
$\mathrm{fib}(n{+}2) = \mathrm{fib}(n{+}1) + \mathrm{fib}(n)$ (exercise
|
|
2.2) and draw the calls made by $\mathrm{fib}(5)$ as a tree, each node
|
|
spawning its two recursive children:
|
|
\[
|
|
\begin{array}{c}
|
|
\mathrm{fib}(5)\\
|
|
\swarrow \qquad\qquad \searrow\\
|
|
\mathrm{fib}(4) \qquad\qquad \mathrm{fib}(3)\\
|
|
\swarrow\;\searrow \qquad\qquad \swarrow\;\searrow\\
|
|
\mathrm{fib}(3)\;\;\mathrm{fib}(2) \qquad \mathrm{fib}(2)\;\;\mathrm{fib}(1)\\
|
|
\dots
|
|
\end{array}
|
|
\]
|
|
Count the visible duplication: $\mathrm{fib}(3)$ is computed twice,
|
|
$\mathrm{fib}(2)$ three times, $\mathrm{fib}(1)$ five times --- the
|
|
\emph{multiplicities are themselves Fibonacci numbers}, which you can
|
|
prove by noticing that $\mathrm{fib}(k)$'s call count obeys the same
|
|
recurrence as $\mathrm{fib}$ itself. Total calls for $\mathrm{fib}(n)$:
|
|
$C(n) = 2\,F_{n+1} - 1$, exponential in $n$ --- the tree is a picture
|
|
of the exponent. Two lessons travel with the picture. First, the
|
|
\emph{value} $\mathrm{fib}(5) = 5$ is independent of the route: an
|
|
efficient two-accumulator loop computes the same function, and
|
|
``the same function'' is exactly the kind of statement Lean can
|
|
\emph{prove} (equal outputs for all inputs) --- specs describe
|
|
meaning, never cost. Second, this meaning/cost split is why
|
|
Chapter~\ref{ch:prime} can let a slow-but-simple definition
|
|
\emph{define} truth while fast code \emph{computes} it, with a theorem
|
|
pinning them together --- the whole verified-crypto architecture in
|
|
embryo, visible in a doodle of $\mathrm{fib}(5)$.
|
|
\end{worked}
|
|
|
|
\section{Structures: records with guarantees}
|
|
|
|
The last data-building tool we need bundles several fields together:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
structure Point where
|
|
x : Int
|
|
y : Int
|
|
|
|
def origin : Point := { x := 0, y := 0 }
|
|
|
|
#eval origin.x -- 0
|
|
\end{lstlisting}
|
|
|
|
A preview of why this matters to us: the Rust type
|
|
\rust{struct FieldElement51(pub [u64; 5])} --- five 64-bit limbs representing
|
|
one element of $\Fp$ --- will arrive in Lean (Chapter~\ref{ch:rust}) as
|
|
essentially a structure holding an array of five machine words. The
|
|
verification question of this entire book is: \emph{do operations on those
|
|
five words faithfully implement arithmetic in $\Fp$?} Structures are how the
|
|
data crosses the bridge.
|
|
|
|
\section{Namespaces, Mathlib, and reading error messages}
|
|
|
|
Real developments organize names in \lean{namespace} blocks
|
|
(\lean{Nat.add}, \lean{Point.x}) and import the mathematical library:
|
|
|
|
\begin{lstlisting}[language=Lean]
|
|
import Mathlib
|
|
open Nat
|
|
|
|
#check Nat.Prime -- the primality predicate, ready-made
|
|
#check ZMod -- integers mod n -- our Chapter 6 home
|
|
\end{lstlisting}
|
|
|
|
And a word of comfort about \emph{error messages}. You will see many. Lean's
|
|
are precise and honest, and the single most useful habit you can develop this
|
|
week is: \textbf{read the expected/actual types in the message, slowly}. A
|
|
message like
|
|
|
|
\begin{lstlisting}
|
|
type mismatch: argument has type Int but is expected to have type Nat
|
|
\end{lstlisting}
|
|
|
|
is not the compiler being difficult; it is a specification violation caught at
|
|
the cheapest possible moment. Verification is this same experience scaled up:
|
|
the machine holds the line, and the line is exactly where you drew it.
|
|
|
|
\begin{tryit}
|
|
Open \code{exercises/Ch02.lean}. It contains the definitions from this chapter
|
|
with a few holes marked \lean{sorry} (a placeholder Lean accepts with a loud
|
|
warning). Replace each with a working definition and watch the warnings
|
|
disappear. In particular: define \lean{mul : Nat -> Nat -> Nat} by recursion,
|
|
using \lean{add} --- the same bootstrapping order (add, then mul) the real
|
|
field proofs follow.
|
|
\end{tryit}
|
|
|
|
\section*{Exercises}
|
|
|
|
\exercise{Define \lean{pow : Nat -> Nat -> Nat} (by recursion on the
|
|
exponent) and check with \lean{\#eval} that \lean{pow 2 10 = 1024}.}
|
|
|
|
\exercise{Define \lean{fib : Nat -> Nat}. Then evaluate \lean{fib 32}. Notice
|
|
the pause --- naive recursion is exponential. (Lean's \lean{\#eval} is fast;
|
|
your algorithm is slow. The distinction matters when we later care about
|
|
\emph{what} is being computed versus \emph{how}.)}
|
|
|
|
\exercise{Write a structure \lean{Rational} with fields \lean{num : Int} and
|
|
\lean{den : Nat}, and a function \lean{Rational.add}. What property of
|
|
\lean{den} can your type \emph{not} enforce yet? (Keep your answer; Chapter~3
|
|
gives you the tool to fix it.)}
|
|
|
|
\exercise{(Paper) Using only the two defining equations of \lean{add},
|
|
write out the full reduction trace of \lean{add 2 3} the way the worked
|
|
example traced \lean{add 3 2}. How many steps does each take? State the
|
|
general rule for the step count of \lean{add m n}.}
|
|
|
|
\section*{Solutions and pathways}
|
|
\solutionsintro
|
|
|
|
\solhead{2.1}
|
|
\pathway Ask: which argument does the recursion consume? An exponent counts
|
|
\emph{how many times} to multiply --- so recurse on it, exactly as
|
|
\lean{add} recursed on ``how many times to take a successor.'' The base
|
|
case is the empty product. If you wrote \lean{pow b 0 = 0}, evaluate
|
|
\lean{pow 2 0} against your algebra ($b^0 = 1$) and let the mismatch teach
|
|
the convention.
|
|
\answer
|
|
\begin{lstlisting}[language=Lean]
|
|
def pow : Nat → Nat → Nat
|
|
| _, Nat.zero => 1
|
|
| b, Nat.succ e => pow b e * b
|
|
\end{lstlisting}
|
|
\lean{\#eval pow 2 10} prints \lean{1024}. Every choice mirrors \lean{add}:
|
|
recursion on the count, base case the operation's identity element ($0$ for
|
|
$+$, $1$ for $\times$) --- a pattern you will reuse for iterated point
|
|
addition in Chapter~\ref{ch:pyramid}.
|
|
|
|
\solhead{2.2}
|
|
\pathway The definition writes itself from the recurrence
|
|
$F_{n+2} = F_{n+1} + F_n$ with two base cases. The interesting part is the
|
|
pause, and it is worth quantifying rather than shrugging at: let $C(n)$
|
|
count the calls needed for \lean{fib n}. Each call recurses twice, so
|
|
$C(n) = C(n{-}1) + C(n{-}2) + 1$ --- the cost obeys (almost) the Fibonacci
|
|
recurrence itself, hence grows like the golden ratio to the $n$.
|
|
\answer
|
|
\begin{lstlisting}[language=Lean]
|
|
def fib : Nat → Nat
|
|
| 0 => 0
|
|
| 1 => 1
|
|
| n + 2 => fib (n + 1) + fib n
|
|
\end{lstlisting}
|
|
One can show $C(n) = 2\,F_{n+1} - 1$; for $n = 32$ that is
|
|
$2 \cdot 3524578 - 1 \approx 7 \times 10^{6}$ calls to produce the
|
|
seven-digit answer $F_{32} = 2178309$ --- the value is cheap, the
|
|
\emph{route} is exponential. Hold on to the distinction: in verification we
|
|
constantly separate \emph{what} a function computes (its spec) from
|
|
\emph{how} (its cost and structure); this is your first taste of the two
|
|
coming apart.
|
|
|
|
\solhead{2.3}
|
|
\pathway The formula $\frac{a}{b} + \frac{c}{d} = \frac{ad + cb}{bd}$
|
|
transcribes directly; the sting is in the question. Try to build a value
|
|
that should not exist.
|
|
\answer
|
|
\begin{lstlisting}[language=Lean]
|
|
def Rational.add (x y : Rational) : Rational :=
|
|
⟨x.num * y.den + y.num * x.den, x.den * y.den⟩
|
|
\end{lstlisting}
|
|
The type cannot enforce $\mathtt{den} \neq 0$: the value \lean{⟨1, 0⟩}
|
|
constructs without complaint, and every function must now either handle it
|
|
or silently misbehave on it. Chapter~\ref{ch:pat} adds a third field ---
|
|
\emph{a proof} \lean{den ≠ 0} stored inside the value --- after which the
|
|
bad value is not handled but \emph{unrepresentable}. The same move, scaled
|
|
up, is the bounds-carrying field element of Chapter~\ref{ch:rust}.
|
|
|
|
\solhead{2.4}
|
|
\pathway Constructor of the second argument decides the equation; count
|
|
how many times you use the second equation before the first fires.
|
|
\answer
|
|
$\mathtt{add}\ 2\ s(s(s(z))) = s(\mathtt{add}\ 2\ s(s(z)))
|
|
= s(s(\mathtt{add}\ 2\ s(z))) = s(s(s(\mathtt{add}\ 2\ z)))
|
|
= s(s(s(2))) = 5$: three uses of the second equation (one per \lean{succ}
|
|
in the second argument), one of the first --- four steps. \lean{add 3 2}
|
|
took three. In general \lean{add m n} takes $n + 1$ steps: $n$ unrollings
|
|
plus the base case. The first argument never influences the step count ---
|
|
it is along for the ride, which is precisely the asymmetry that will make
|
|
\lean{0 + n = n} a theorem needing induction rather than a computation.
|
|
|
|
\begin{checkpoint}
|
|
You should now be able to: evaluate and type-check expressions with
|
|
\lean{\#eval}/\lean{\#check}; define functions, including recursive ones over
|
|
\lean{Nat}; explain what an inductive type is and recite the two constructors
|
|
of \lean{Nat}; and state from memory what \lean{Nat} subtraction does on
|
|
$3 - 5$, and why that will matter.
|
|
\end{checkpoint}
|