Alexandre Rademaker
FGV/EMAp | CSLib at Renaissance Philanthropy
INRIA Paris 2026
s2n-bignum is a collection of integer arithmetic routines designed for cryptographic applications. All routines are written in pure machine code, designed to be callable from C and other high-level languages, with separate but API-compatible versions of each function for 64-bit x86 (x86_64) and ARM (aarch64).
https://github.com/awslabs/s2n-bignum
Primary goals: performance and assurance
Elementary 64-bit word operations
Generic bignum arithmetic: addition, subtraction, multiplication, comparison
Constant-time data manipulation
Montgomery operations for modular arithmetic
Optimized operations for elliptic curves:
Curve25519, NIST P-256/384/521, secp256k1, SM2
The actual sequence of machine instructions executed, including the specific addresses and sequencing of memory loads and stores, is independent of the numbers themselves, depending only on their nominal sizes.
Consequences:
No stripping of leading zeros, no dynamic memory allocation
Results that do not fit are truncated modulo the output size
Sizes are explicit parameters — fixed at call time
void bignum_mul(uint64_t p, uint64_t *z,
uint64_t m, uint64_t *x,
uint64_t n, uint64_t *y);
x is an m-digit bignum, y is n-digit; result written to p-word buffer z.
Each function ships with a machine-checked proof in HOL Light:
Proves the mathematical result is correct
Based on a formal model of the underlying processor (ARM or x86)
The model specifies exactly how each instruction modifies registers, flags, and memory
Two logics, both mechanized in HOL Light:
L1 (unary): functional correctness via Hoare triples — eventually operator, ~860K lines of proofs covering 600+ routines. It is implemented in 10k lines of HOL Light.
L2 (relational): extends L1 with step-counting eventually to prove constant-time behavior and equivalence between optimized and reference implementations. The core of the relational verification amounts to 1704 lines of code.
Relational Hoare Logic for Realistically Modelled Machine Code.
Dan J. Bernstein (creator of elliptic curve Curve25519) "OpenSSL/BoringSSL must use s2n-bignum!"
s2n-bignum/
├── arm/
│ ├── proofs/ # ~351 HOL Light files
│ │ ├── bignum_add.ml
│ │ ├── bignum_mul.ml
│ │ └── ...
│ └── tutorial/ # introductory examples
│ ├── simple.ml
│ └── sequence.ml
├── x86/
│ └── proofs/ # x86-64 proofs (similar structure)
└── common/
└── bignum.ml # core definitions
Because John Harrison is the creator of HOL Light!
HOL Light is an interactive theorem prover, like Lean

Lean 4 advantages:
Same language for code and proofs — no translation layer
Mathlib: 2M+ lines of mathematics, growing fast
Active community, modern tooling (Lake, LSP, VS Code)
Lean is implemented in Lean — highly extensible
Performance
Goal: make this verification infrastructure accessible to the broader Lean community
32 general-purpose registers, flags, memory, program counter
public abbrev Word64 := BitVec 64
abbrev Address := Word64
def Memory := Address → Option UInt8
inductive Reg
| X : Fin 31 → Reg -- X0–X30
| PC : Reg
| SP : Reg
structure ArmState where
regs : Slide.Reg → Word64
flags : Flags
mem : Memory
Memory (List UInt8)
│
▼
arm_decode (Spec.lean) -- reads 4 bytes at PC
└─→ decode : UInt32 → -- Decode.lean
Option Instruction
│
▼
step : Instruction → -- Instruction.lean
ArmState → ArmState
├─→ read_reg / write_reg -- State.lean
└─→ advance_pc
│
▼
exec : List Instruction → -- folds step over list
ArmState → ArmState
│
▼
arm : ArmState → ArmState → -- single-step relation
Prop -- used in ensures triples
ensures_of_exec bridges exec and arm, reducing Hoare
proofs to: (1) decode, (2) post-condition, (3) frame.
Execution is instruction-by-instruction:
def advance_pc (s : ArmState) (s' : ArmState) : ArmState :=
s'.write_reg Reg.PC (s.read_reg Reg.PC + 4)
def step (instr : Instruction) (s : ArmState) : ArmState :=
match instr with
| Instruction.ADD rd rn rm =>
-- Xd := Xn + Xm (word addition, no flags)
let val_n := s.read_reg rn
let val_m := s.read_reg rm
let result := val_n + val_m -- BitVec addition (wraps at 2^64)
advance_pc s (s.write_reg rd result)
...
We use ensures triples — Hoare logic adapted to machine code:
def ensures (step : α → α → Prop) (pre post : α → Prop)
(frame : α → α → Prop) : Prop :=
∀ s, pre s → eventually step
(fun s' => post s' ∧ frame s s') s
pre: precondition on initial state
post: relates initial and final state (allows frame reasoning)
frame: memory and registers not mentioned are unchanged
Programs compose sequentially:
theorem ensures_sequence
(pre mid post : ArmState → Prop)
(prog₁ prog₂ : List UInt8) :
ensures pre prog₁ mid →
ensures mid prog₂ post →
ensures pre (prog₁ ++ prog₂) post
This enables modular proofs — verify each chunk independently.
HOL Light (OCaml meta-language):
let HIGH_LOW_DIGITS = prove
(`(!n i. 2 EXP (64 * i) * highdigits n i + lowdigits n i = n) /\
(!n i. lowdigits n i + 2 EXP (64 * i) * highdigits n i = n) /\
(!n i. highdigits n i * 2 EXP (64 * i) + lowdigits n i = n) /\
(!n i. lowdigits n i + highdigits n i * 2 EXP (64 * i) = n)`,
REWRITE_TAC[lowdigits; highdigits] THEN
MESON_TAC[DIVISION_SIMP; ADD_SYM; MULT_SYM]);;
Lean 4 (same language as definitions):
/--
Compositionality of highdigits: taking highdigits twice is equivalent to adding
indices.
Source: s2n-bignum/common/bignum.ml:169-172
-/
theorem highdigits_highdigits (n i j : Nat) :
highdigits (highdigits n i) j = highdigits n (i + j) := byn:ℕi:ℕj:ℕ⊢ highdigits (highdigits n i) j = highdigits n (i + j)
unfold highdigitsn:ℕi:ℕj:ℕ⊢ n / 2 ^ (64 * i) / 2 ^ (64 * j) = n / 2 ^ (64 * (i + j))
rw [Nat.mul_add, Nat.pow_add, Nat.div_div_eq_div_mul]All goals completed! 🐙
Every Lean definition maps back to its HOL Light source equivalence.
bignum-lean/
├── Bignum/
│ ├── Common/
│ │ ├── Basic/
│ │ │ ├── Defs.lean # bigdigit, highdigits, lowdigits
│ │ │ └── Lemmas.lean # core theorems (complete)
│ │ ├── Word.lean # BitVec 64 arithmetic
│ │ └── Memory.lean # memory model
│ └── Arm/
│ ├── Machine/ # State, Instruction, Decode, Loader
│ ├── Spec.lean # ensures + operational semantics
│ ├── Tactic.lean # proof automation
│ └── Tutorial/ # Simple.lean, Sequence.lean
└── s2n-bignum/ # original HOL Light (git submodule)
Port of s2n-bignum/arm/tutorial/simple.ml
0: 8b000022 add x2, x1, x0
4: cb010042 sub x2, x2, x1
Starting with X0 = a, X1 = b, after both instructions: X2 = a. Only PC and X2 change.
theorem SIMPLE_SPEC (pc a b : ℕ) :
ensures arm
(fun s => aligned_bytes_loaded s.mem (BitVec.ofNat 64 pc) simple_mc ∧
s.read_reg Reg.PC = BitVec.ofNat 64 pc ∧
s.read_reg Reg.X0 = BitVec.ofNat 64 a ∧
s.read_reg Reg.X1 = BitVec.ofNat 64 b)
(fun s => s.read_reg Reg.PC = BitVec.ofNat 64 (pc + 8) ∧
s.read_reg Reg.X2 = BitVec.ofNat 64 a)
(maychange_regs [Reg.PC, Reg.X2]) := by
Decode: bytes → Instruction list (2 lines)
Post: postcondition holds after exec (10 lines)
Frame: other registers unchanged (5 lines)
Port of s2n-bignum/arm/tutorial/sequence.ml
0: 8b000021 add x1, x1, x0
4: 8b000042 add x2, x2, x0
8: d2800043 mov x3, #0x2
c: 9b037c21 mul x1, x1, x3
Split at pc+8 into two chunks:
First chunk (pc to pc+8): two add instructions
Second chunk (pc+8 to pc+16): mov and mul
Intermediate assertion at pc+8: X1 = a + b
Prove each chunk with ensures_of_exec, compose with ensures_sequence.
theorem sequence_correct (pc a b c : ℕ) :
ensures arm
(sequence_pre pc a b c)
(sequence_post pc a b)
(maychange_regs [Reg.PC, Reg.X1, Reg.X2, Reg.X3]) :=
ensures_sequence _ _ _
(maychange_regs [Reg.PC, Reg.X1, Reg.X2, Reg.X3])
(sequence_chunk1_correct pc a b c)
(sequence_chunk2_correct pc a b)
(maychange_regs_trans _)
Original HOL Light: ~185 lines. Lean port: ~100 lines.
First version of ARM architecture
First two tutorials completed
Building the Lean project is faster than load HOL Light
Connect to CSLib
Connect to Mathlib
End-to-end proofs from algorithm to assembly
Port ARM arithmetic algorithm proofs
bignum_add, bignum_mul, bignum_sub, modular reduction...
Revise infrastructure (Spec, Tactic, Decode) in place
x86-64 support
Adapt the ARM machine model for x86-64
Port the x86 proofs from s2n-bignum
Relational verification (see the paper)
Constant-time proofs
Functional equivalence between optimized and verification-friendly routines
Alexandre Rademaker
FGV/EMAp | CSLib
github.com/atlas-computing-org/bignum
arademaker@gmail.com