verifying-crypto-with-lean/chapters/ch09-denotation-bridge.tex

373 lines
18 KiB
TeX
Raw Normal View History

\chapter{The Denotation Bridge: What Do Five Numbers \emph{Mean}?}
\label{ch:denotation}
\section{Two worlds, one bridge}
We now hold two very different objects. On one side, mathematics:
the field $\Fp$, where elements are abstract clock positions and $+$ means
ideal modular addition. On the other side, the extracted model: arrays of
five \lean{U64} words, shuffled by loads, adds, and stores. The entire
question of verified cryptography is how to say --- precisely --- that the
second \emph{implements} the first.
The answer is a single function, small enough to write on one line and
important enough to carry this whole book. Given limbs
$a = (a_0, a_1, a_2, a_3, a_4)$, define the \textbf{denotation}:
\[
\denote{a} \;=\; a_0 + 2^{51} a_1 + 2^{102} a_2 + 2^{153} a_3 + 2^{204} a_4
\;\in\; \Fp .
\]
Read $\denote{a}$ as ``the field element these limbs \emph{mean}.'' It is
positional notation, nothing more --- base $2^{51}$ instead of base 10, with
the result interpreted on the clock face of $\Fp$. In Lean:
\begin{lstlisting}[language=Lean]
def denote (a : Array U64 5) : ZMod p :=
a[0].val + 2^51 * a[1].val + 2^102 * a[2].val
+ 2^153 * a[3].val + 2^204 * a[4].val
\end{lstlisting}
With the bridge in hand, correctness of an operation becomes a
\emph{commuting square} --- one picture you should internalize until you see
it in your sleep:
\begin{center}
\begin{tikzpicture}[
world/.style={font=\small,align=center},
arr/.style={-{Stealth},thick,ink2},
lbl/.style={font=\small\color{ink2}}
]
\node[world] (tl) at (0,2.6) {$(a, b)$\\ \footnotesize limb arrays};
\node[world] (tr) at (7.2,2.6) {$\mathtt{add}(a,b)$\\ \footnotesize limb array};
\node[world] (bl) at (0,0) {$(\denote{a}, \denote{b})$\\ \footnotesize field elements};
\node[world] (br) at (7.2,0) {$\denote{a} + \denote{b}$\\ \footnotesize field element};
\draw[arr] (tl) -- node[lbl,above] {machine code} (tr);
\draw[arr] (bl) -- node[lbl,below] {ideal math} (br);
\draw[arr] (tl) -- node[lbl,left] {$\denote{\cdot}$} (bl);
\draw[arr] (tr) -- node[lbl,right] {$\denote{\cdot}$} (br);
\node[font=\small\color{accent},align=center] at (3.6,1.3)
{\textbf{the theorem:}\\ both routes agree};
\end{tikzpicture}
\end{center}
\begin{bigidea}
\textbf{The correctness of an implementation is the statement that
denotation commutes with every operation:}
\[
\denote{\mathtt{add}(a,b)} = \denote{a} + \denote{b},
\qquad
\denote{\mathtt{mul}(a,b)} = \denote{a} \cdot \denote{b},
\qquad\dots
\]
The left-hand side lives in the machine world (with its bounds hypotheses
and \lean{Result}s); the right-hand side is pure mathematics. One equation
per operation, and the ugly optimized code is pinned, forever, to the
textbook meaning. Every verified-crypto project you will ever read is this
diagram, instantiated.
\end{bigidea}
\section{Why redundancy is freedom (and where bugs hide)}
A subtlety with consequences: denotation is \textbf{many-to-one}. The limb
arrays $(19, 0, 0, 0, 0)$ and $(p + 19 \bmod 2^{\cdots}, \dots)$ --- or more
mundanely, unreduced sums whose limbs exceed $2^{51}$ --- can denote the
\emph{same} field element. The representation has slack, and the
implementation \emph{exploits} it: the fast \code{add} from
Chapter~\ref{ch:rust} just adds limbs pairwise, letting values drift above
$2^{51}$, and nobody reduces until a cheaper moment. The commuting square
still closes because $\denote{\cdot}$ doesn't care how bloated the limbs are
--- positional value is positional value.
\begin{worked}{denoting the prime itself --- a telescope in radix 51}
The canonical example of a collision uses the most important limb array in
the codebase: the representation of $p$ itself,
\[
P \;=\; (2^{51}-19,\; 2^{51}-1,\; 2^{51}-1,\; 2^{51}-1,\; 2^{51}-1).
\]
Claim: $\denote{P} = p \equiv 0$, so $P$ collides with
$(0,0,0,0,0)$. Verify by hand --- the sum telescopes beautifully. Write
out the positional value:
\[
\denote{P}_{\Z} = (2^{51}-19) + (2^{51}-1)2^{51} + (2^{51}-1)2^{102}
+ (2^{51}-1)2^{153} + (2^{51}-1)2^{204}.
\]
Expand each product: $(2^{51}-1)2^{51k} = 2^{51(k+1)} - 2^{51k}$. So the
sum is
\[
(2^{51} - 19) + (2^{102} - 2^{51}) + (2^{153} - 2^{102})
+ (2^{204} - 2^{153}) + (2^{255} - 2^{204}).
\]
Every power except the last cancels against its neighbor --- slide a pen
along the chain and watch $2^{51}, 2^{102}, 2^{153}, 2^{204}$ annihilate
--- leaving
\[
\denote{P}_{\Z} = 2^{255} - 19 = p \;\equiv\; 0 \pmod p. \qquad
\]
This computation is why the constant array $P$ (and its multiple $16P$,
Chapter~\ref{ch:lean}'s worked example) appears throughout the dalek
source: adding $P$ limb-wise changes nothing denotationally, and that
``nothing'' is exactly the freedom the subtraction trick spends. When the
verified \code{sub} spec says \lean{denote (sub a b) = denote a - denote b},
the proof's central move is this telescope, done once, as a lemma.
\end{worked}
This is also exactly where the carry bugs of Chapter~\ref{ch:why} live: code
that is correct only while the drift stays within headroom, and wrong on the
rare inputs where it spills. In the verified development that danger becomes
a visible, machine-checked pair of clauses attached to every operation:
\begin{lstlisting}[language=Lean]
theorem add_spec (ha : Bnd54 a) (hb : Bnd54 b) :
∃ c, add a b = .ok c
∧ Bnd55 c -- (1) bounds: the envelope holds
∧ denote c = denote a + denote b -- (2) value: the meaning is right
\end{lstlisting}
Clause (2) is the commuting square. Clause (1) feeds the \emph{next}
operation's hypothesis --- correctness composes only because every theorem
hands the following one the envelope it requires. A chain of such specs is
the formal skeleton of ``this sequence of optimized operations computes the
formula we claim.''
\begin{aha}
The denotation idea is vastly older and bigger than cryptography. Compilers
prove ``optimized code means the same as naive code''; databases prove
``this query plan means the same query''; hardware verifies ``this pipelined
circuit means this instruction set.'' The pattern --- map both sides into a
mathematical meaning-space and prove the square commutes --- is called
\emph{denotational semantics}, and you have now used it for real. It is the
single most transferable idea in this book.
\end{aha}
\section{Multiplication: where the bridge earns its keep}
Addition's square closes in an afternoon. Multiplication is the boss fight,
and seeing \emph{why} teaches you what verified arithmetic is really like.
Schoolbook multiplication of two 5-limb numbers produces nine columns of
partial products $\sum_{i+j=k} a_i b_j$; each column then owes a
\emph{carry} to the next; and columns $k \ge 5$ --- weights $2^{255}$ and up
--- must be folded back using the Chapter~\ref{ch:modular} identity
$2^{255} \equiv 19$. The implementation interleaves all three concerns for
speed. The proof must un-interleave them:
\[
\denote{\mathtt{mul}(a,b)}
\;\overset{?}{=}\;
\Big(\textstyle\sum_{k=0}^{8} 2^{51k} \sum_{i+j=k} a_i b_j \Big) \bmod p
\;\overset{?}{=}\; \denote{a} \cdot \denote{b} .
\]
The right equality is algebra --- \lean{ring} territory. The left is a walk
through the extracted code: every intermediate \lean{U128} product bounded
(no overflow --- the $2^{54}$ headroom at work), every carry accounted,
every $\times 19$ fold placed. In the companion projects this is a long
\lean{calc}-and-\lean{have} museum: dozens of small steps, each dispatched
by \lean{omega} or a bound lemma, composed into one commuting square.
\begin{worked}{the $\times 19$ fold, derived at the real weights}
The fold is where students usually first believe the whole enterprise is
black magic; five lines of weight arithmetic dispel it, at full scale.
Schoolbook multiplication of $a$ and $b$ (limbs $a_0..a_4$, $b_0..b_4$)
produces columns $c_k = \sum_{i+j=k} a_i b_j$ at weights $2^{51k}$ for
$k = 0, \dots, 8$. Columns $5$--$8$ carry weights $2^{255}, 2^{306},
2^{357}, 2^{408}$ --- all off the top of the representation. Reduce each
weight with the Chapter~\ref{ch:modular} identity
$2^{255} \equiv 19$:
\[
2^{51(k+5)} \;=\; 2^{255} \cdot 2^{51k} \;\equiv\; 19 \cdot 2^{51k}
\pmod{p},
\qquad k = 0, 1, 2, 3 .
\]
So column $5$ re-enters at weight $2^{0}$ scaled by $19$, column $6$ at
weight $2^{51}$ scaled by $19$, and so on --- each overflow column lands
exactly one radix position below where it left, times $19$. The folded
five-column result is therefore
\[
\begin{array}{lcl}
r_0 &=& c_0 + 19\, c_5\\
r_1 &=& c_1 + 19\, c_6\\
r_2 &=& c_2 + 19\, c_7\\
r_3 &=& c_3 + 19\, c_8\\
r_4 &=& c_4
\end{array}
\]
--- which, written with the products expanded ($r_0 = a_0 b_0 +
19(a_1 b_4 + a_2 b_3 + a_3 b_2 + a_4 b_1)$, etc.), is
\emph{character-for-character} the mysterious formula block in
\code{curve25519-dalek}'s \code{mul}, the one decorated with the comment
``see the comment above'' that every reader of that file has squinted at.
You have now derived it. Two bound checks make it safe, both already
yours: each $19 c_k < 2^{5} \cdot 2^{111} = 2^{116}$ (the
Chapter~\ref{ch:automation} column bound plus five bits for the $19$),
still comfortably inside a \lean{U128}. And the formal proof of
\lean{mul}'s commuting square is precisely this box: the four weight
identities as \lean{have}s, the bound checks as \lean{omega} goals, and
\lean{ring} to shuffle the expanded polynomial.
\end{worked}
One more representational dialect, because you will meet it in the Pasta
repos: \textbf{Montgomery form} stores $x$ as $x \cdot R \bmod p$ (with
$R = 2^{256}$) because it makes reduction after multiplication cheap. The
bridge absorbs the twist without complaint --- define
$\denote{a}_{\mathrm{M}} = (\text{positional value of } a) \cdot R^{-1}$
and the same commuting squares govern everything. Denotation is a
\emph{policy about meaning}, and it bends to fit the representation, not
the other way around.
\begin{worked}{the cast --- moving an equation from $\N$ to the clock face}
Every denotation proof ends with the same quiet move --- ``now reduce
mod $p$'' --- and in a proof assistant that move must be performed, not
gestured at. Here is the by-hand version of what \lean{push_cast} and
friends do, on the Interlude's own equation. You hold an \emph{exact}
identity of natural numbers:
\[
\underbrace{v}_{\text{output value}} + 15\,c_2
= \underbrace{(a_0 + 4a_1)}_{\denote{a}\text{'s integer}} +
\underbrace{(b_0 + 4b_1)}_{\denote{b}\text{'s integer}}
\qquad\text{in } \N .
\]
You want its shadow in $\Zmod{15}$. The bridge is the \emph{cast
homomorphism} $\iota : \N \to \Zmod{15}$ (send each number to its clock
position), and the three facts that make it usable:
(i)~$\iota(x + y) = \iota(x) + \iota(y)$ and
$\iota(x \cdot y) = \iota(x)\,\iota(y)$ --- casting commutes with
arithmetic, so the equation's \emph{shape} survives;
(ii)~equal naturals cast to equal clock positions --- so the equation's
\emph{truth} survives; (iii)~$\iota(15) = 0$ --- the modulus dies.
Apply (ii), then distribute $\iota$ through both sides by (i), then
kill the $15$ by (iii):
\[
\iota(v) + 0 \cdot \iota(c_2) = \iota(a_0) + 4\,\iota(a_1) +
\iota(b_0) + 4\,\iota(b_1)
\quad\Longrightarrow\quad
\denote{\text{out}} = \denote{a} + \denote{b}. \;\blacksquare
\]
That is all \lean{push_cast} does: drive $\iota$ inward through $+$ and
$\times$ by fact (i), normalizing the statement so facts (ii)/(iii) can
fire. Knowing the hand version buys you two things. You can
\emph{predict} when the tactic will fail --- fact (i) has no clause for
subtraction or division in $\N$ (truncation! Chapter~\ref{ch:lean}), so
casts do not push through them, which is precisely why the proof
pathway insists on an exact identity with the correction term
\emph{added on the left} rather than subtracted on the right. And you
can \emph{read} the real proofs' cast steps as what they are: the
one-line ceremony where a theorem about machine words becomes a theorem
about field elements --- the bridge being crossed, visibly, in both
media.
\end{worked}
\begin{pitfall}
When a denotation proof refuses to close, the failure is information ---
read it like a detective, in order: (1) Is the \emph{bound} hypothesis
strong enough for the intermediate products? (Count bits, on paper.)
(2) Is the \emph{denotation} right for this representation --- radix, limb
count, Montgomery factor? (3) Only then suspect the code. In the companion
projects this checklist ran hundreds of times; its order reflects the actual
base rates of what was wrong.
\end{pitfall}
\begin{tryit}
Open \code{exercises/Ch09.lean}. It builds a miniature of the whole story
you can hold in your head: a \emph{2-limb, radix-4} representation of
$\Zmod{15}$ (limbs are values $0$--$3$, denotation $a_0 + 4a_1$, and
$16 \equiv 1$ makes the fold trivial). You will write \lean{denote}, prove
the commuting square for the provided \lean{add} with carry, then for
\lean{mul} with its fold --- every conceptual ingredient of the dalek proof,
at a scale where \lean{decide} can double-check your work.
\end{tryit}
\section*{Exercises}
\exercise{Compute by hand the denotation of the limb arrays $(19,0,0,0,0)$
and $(0,0,0,0,2^{51})$ in the radix-51 system, reducing mod $p = 2^{255}-19$.
Conclude that $\denote{\cdot}$ is not injective by exhibiting the collision.}
\exercise{In the mini-system of the Try It box, find two distinct limb pairs
denoting the same element of $\Zmod{15}$, and check that the provided
\lean{add} treats them interchangeably \emph{as far as denotation goes} ---
compute both sides.}
\exercise{Sketch the multiplication column sums $\sum_{i+j=k} a_i b_j$ for
the 2-limb system and carry out the fold $16 \equiv 1$ by hand for
$a = (3,2)$, $b = (1,3)$. Check against direct computation in $\Zmod{15}$.}
\exercise{(Paper, challenge) For the radix-51 system with limbs bounded by
$2^{54}$: bound one column $\sum_{i+j=4} a_i b_j$ of partial products and
confirm it fits a \lean{U128}. How much headroom remains? This number ---
not elegance --- is why the invariant chose $2^{54}$.}
\section*{Solutions and pathways}
\solutionsintro
\solhead{9.1}
\pathway Compute both denotations as integers, then reduce mod $p$. One of
them should ring a bell from the reduction identity.
\answer $\denote{(19,0,0,0,0)}_{\Z} = 19$. For $(0,0,0,0,2^{51})$: the
top limb sits at weight $2^{204}$, so the value is
$2^{51} \cdot 2^{204} = 2^{255} \equiv 19 \pmod p$ --- the crown identity
of Chapter~\ref{ch:modular} again. Two visibly different arrays, one
field element: $\denote{\cdot}$ is not injective, witnessed by the pair
$\big((19,0,0,0,0),\, (0,0,0,0,2^{51})\big)$. Note that the second array
even violates the ``reduced'' bound ($2^{51} \not< 2^{51}$) --- it is
exactly the kind of drifted-but-meaningful value lazy carries produce,
and the denotation handles it without complaint.
\solhead{9.2}
\pathway In the toy system, hunt for a nonzero array whose positional
value is a multiple of $15$. The largest representable value is
$3 + 4\cdot 3 = 15$ itself --- convenient.
\answer $\denote{(3,3)} = 3 + 4 \cdot 3 = 15 \equiv 0 = \denote{(0,0)}$.
Interchangeability under \lean{add} with, say, $(1,2)$: the machine runs
differ --- $\mathtt{add}\ (3,3)\ (1,2)$ produces carries, $\mathtt{add}\
(0,0)\ (1,2)$ does not --- but both outputs denote
$0 + (1 + 4\cdot 2) = 9$. (With the definitions in
\code{exercises/Ch09.lean}: $\mathtt{add}\ (3,3)\ (1,2)$: $s_0 = 4$,
$s_1 = 3 + 2 + 1 = 6$, output $(0 + 1, 2) = (1,2)$, denotation $9$ ✓;
$\mathtt{add}\ (0,0)\ (1,2) = (1,2)$, denotation $9$ ✓.) The spec never
promised equal \emph{arrays} --- only equal \emph{meanings}; representation
freedom is preserved by every operation, which is the entire content of
``the square commutes.''
\solhead{9.3}
\pathway Write the three columns of the $2\times 2$ schoolbook product,
fold the top one with $16 \equiv 1$, then cross-check against a direct
computation in $\Zmod{15}$ --- at toy scale you can afford both routes,
which is the point of having a toy.
\answer For $a = (3,2)$, $b = (1,3)$: columns
$c_0 = a_0 b_0 = 3$, $c_1 = a_0 b_1 + a_1 b_0 = 9 + 2 = 11$,
$c_2 = a_1 b_1 = 6$ at weights $1, 4, 16$. Fold: $16 \equiv 1$, so $c_2$
re-enters at weight $1$:
\[
\text{value} \equiv (c_0 + c_2) + 4 c_1 = 9 + 44 = 53 \equiv 53 - 45 = 8
\pmod{15}.
\]
Direct route: $\denote{a} = 3 + 8 = 11$, $\denote{b} = 1 + 12 = 13$;
$11 \cdot 13 = 143 = 9 \cdot 15 + 8 \equiv 8$ ✓. Both routes, one
answer --- you have closed a commuting square numerically. The Lean
theorem \lean{mulVal_spec} is this computation with the numbers replaced
by universally quantified variables: same fold, all inputs at once.
\solhead{9.4}
\pathway This is the Chapter~\ref{ch:automation} worked example ---
reconstruct it from memory before checking back.
\answer Each product $a_i b_j < 2^{54} \cdot 2^{54} = 2^{108}$; column
$c_4$ has five of them, so $c_4 < 5 \cdot 2^{108} < 2^{111} < 2^{128}$,
leaving $17$ bits of headroom --- consumed, in the real code, by the
carry-in from the previous column and the $\times 19$ fold factor
($19 < 2^5$: the fold costs five of the seventeen bits, which is why the
margin looks generous and is not). Run the same audit at bound $2^{55}$:
$5 \cdot 2^{110} < 2^{113}$, fold to $2^{118}$ --- still fits, but the
\emph{addition} chain budget halves; at $2^{63}$: $5 \cdot 2^{126} >
2^{128}$ --- broken outright. The invariant $2^{54}$ is where the
operations' competing demands settle, and now you have audited the
settlement yourself.
\begin{checkpoint}
You should now be able to: write the radix-51 denotation from memory; draw
the commuting square and label which side owns bounds and \lean{Result}s;
explain why many-to-one representation is both the performance trick and
the bug habitat; and recognize the two-clause shape (bounds propagation +
value equation) as the universal skeleton of implementation-correctness
theorems.
\end{checkpoint}