We will present this idea in detail and illustrate how it works for a very simple example.
We typically use formal verification for critical applications, where either:
With formal verification, in theory, we can guarantee that the software will never fail, as we can check all possible cases for a given property. A property can be that no nonadmin users can read sensitive data, or that a program never fails with uncaught exceptions. For that to be truly the case, we need to verify the whole software stack for all the relevant properties.
In this research paper Finding and Understanding Bugs in C Compilers, no bugs were found in the middleend of the formally verified CompCert C compiler, while the other C compilers (GCC, LLVM, ...) all contained subtle bugs. This illustrates that formal verification can be an effective way to make complex software with zero bugs!
To be able to reason on a program we go back to the definition of programming languages. The programming languages (C, JavaScript, Python, ...) are generally defined with a precise set of rules. For example, in Python, the if
statement is defined in the reference manual by:
if_stmt ::= "if" assignment_expression ":" suite
("elif" assignment_expression ":" suite)*
["else" ":" suite]
It selects exactly one of the suites by evaluating the expressions one by one until one is found to be true (see section Boolean operations for the definition of true and false); then that suite is executed (and no other part of the if statement is executed or evaluated). If all expressions are false, the suite of the else clause, if present, is executed.
— The Python's reference manual
This means that the Python code:
if condition:
a
else:
b
will execute a
when the condition
is true, and b
otherwise. There are similar rules for all other program constructs (loops, function definitions, classes, ...).
To make these rules more manageable, we generally split them into two parts:
if
statement.In formal verification, we will focus on the semantics of programs, assuming that the syntax is already verified by the compiler or interpreter, generating "syntax errors" in case of illformed programs.
We consider this short Python example of a function returning the maximum number in a list:
def my_max(l):
m = l[0]
for x in l:
if x > m:
m = x
return m
We assume that the list l
is not empty and only contains integers. If we run it on a few examples:
my_max([1, 2, 3]) # => 3
my_max([3, 2, 1]) # => 3
my_max([1, 3, 2]) # => 3
it always returns 3
, the biggest number in the list! But can we make sure this is always the case?
We can certainly not run my_max
on all possible lists of integers, as there are infinitely many of them. We need to reason from the definition of the Python language, which is what we call formal verification reasoning.
Here is a general specification that we give of the my_max
function above:
forall (index : int) (l : list[int]),
0 ≤ index < len(l) ⇒
l[index] ≤ my_max(l)
It says that for all integer index
and list of integers l
, if the index is valid (between 0
and the length of the list), then the element at this index is less than or equal to the maximum of the list that we compute.
To verify this property for all possible list l
, we reason by induction. A nonempty list is either:
At the start of the code, we will always have:
def my_max(l):
m = l[0]
with m
being equal to the first item of the list. Then:
for
loop, with x
equal to l[0]
. The condition:
if x > m:
if l[0] > l[0]:
m = l[0]
, which is the only element of the list, and it verifies our property as:
l[0] ≤ l[0]
for
loop and iterate over all the elements until the last one. Our induction hypothesis tells us that the property we verify is true for the first part of the list, excluding the last element. This means that:
l[index] ≤ m
index
between 0
and len(l)  2
. When we reach the last element, we have:
if x > m:
m = x
x
being l[len(l)  1]
. There are two possibilities. Either (i) x
is less than or equal to m
, and we do not update m
, or (ii) x
is greater than m
, and we update m
to x
. In both cases, the property is verified for the last element of the list, as:
m
stays the same, so it is still larger or equal to all the elements of the list except the last one, as well as larger or equal to the last one according to this last if
statement.m
is updated to x
, which is the last element of the list and a greater value than the original m
. Then it means that m
is still larger or equal to all the elements of the list except the last one, being larger that the original m
, and larger or equal to the last one as it is in fact equals to the last one.We have now closed our induction proof and verified that our property is true for all possible lists of integers! The reasoning above is rather verbose but should actually correspond to the intuition of most programmers when reading this code.
In practice, with formal verification, the reasoning above is done in a proof assistance such as Coq to help making sure that we did not forget any case, and automatically solve simple cases for us. Having a proof written in a proof language like Coq also allows us to rerun it to check that it is still valid after a change in the code, and allows thirdparty persons to check it without reading all the details.
An additional property that we did not verify is:
forall (l : list[int]),
exists (index : int),
0 ≤ index < len(l) and
l[index] = my_max(l)
It says that the maximum of the list is actually in the list. We can verify it by induction in the same way as we did for the first property. You can detail this verification as an exercise.
If you want to go into more details for the formal verification of Python programs, you can look at our coqofpython project, where we define the semantics of Python in Coq and verify properties of Python programs (ongoing project!). We also provide formal verification services for Rust and other languages like OCaml. Contact us at contact@formal.land to discuss if you have critical applications to check!
We have presented here the idea of formal verification, a technique to verify the absence of bugs in a program by reasoning from first principles. We have illustrated this idea for a simple Python example, showing how we can verify that a function computing the maximum of a list is correct for all possible lists of integers.
We will continue with more blog posts explaining what we can do with formal verification and why it matters. Feel free to share this post and to tell us what subjects you want to see covered!
]]>We will show in this article how we can merge the steps 2. and 3. to save time in the verification process. We do so by relying on the proof mode of Coq and unification.
Our midterm goal is to formally specify the Ethereum Virtual Machine (EVM) and prove that this specification is correct according to reference implementation of the EVM in Python. This would ensure that it is always uptodate and exhaustive. The code of this project is opensource and available on GitHub: formalland/coqofpython.
We put the Python code that we import in Coq in a monad M
to represent all the features that are hard to express in Coq, mainly the side effects. This monad is a combination of two levels:
LowM
for the side effects except the control flow.M
that adds an error monad on top of LowM
to handle the control flow (exceptions, break
instruction, ...).Here is the definition of the LowM
monad in CoqOfPython.v:
Module Primitive.
Inductive t : Set > Set :=
 StateAlloc (object : Object.t Value.t) : t (Pointer.t Value.t)
 StateRead (mutable : Pointer.Mutable.t Value.t) : t (Object.t Value.t)
 StateWrite (mutable : Pointer.Mutable.t Value.t) (update : Object.t Value.t) : t unit
 GetInGlobals (globals : Globals.t) (name : string) : t Value.t.
End Primitive.
Module LowM.
Inductive t (A : Set) : Set :=
 Pure (a : A)
 CallPrimitive {B : Set} (primitive : Primitive.t B) (k : B > t A)
 CallClosure {B : Set} (closure : Data.t Value.t) (args kwargs : Value.t) (k : B > t A)
 Impossible.
Arguments Pure {_}.
Arguments CallPrimitive {_ _}.
Arguments CallClosure {_ _}.
Arguments Impossible {_}.
Fixpoint bind {A B : Set} (e1 : t A) (e2 : A > t B) : t B :=
match e1 with
 Pure a => e2 a
 CallPrimitive primitive k => CallPrimitive primitive (fun v => bind (k v) e2)
 CallClosure closure args kwargs k => CallClosure closure args kwargs (fun a => bind (k a) e2)
 Impossible => Impossible
end.
End LowM.
This is a monad defined by continuation (the variable k
):
Pure
and some result a
, that can be any purely functional expression.Primitive.t
that are side effects:
StateAlloc
to allocate a new object in the memory,StateRead
to read an object from the memory,StateWrite
to write an object in the memory,GetInGlobals
to read a global variable, doing name resolution. This is a side effects as function definitions in Python do not need to be ordered.CallClosure
. This is required for termination, as we cannot define an eval function on the type of Python values since some do not terminate like the Ω expression. See our previous post Translation of Python code to Coq for our definition of Python values. The combinator CallClosure
is also very convenient to modularize our proofs: we reason on each closure independently.Impossible
.The final monad M
is defined as:
Definition M : Set :=
LowM.t (Value.t + Exception.t).
It has no parameters as Python is untyped, so all expressions have the same result type:
Value.t
,Exception.t
, with some special cases to represent a return
, a break
, or a continue
instruction.We define the monadic bind of M
like for the error monad:
Definition bind (e1 : M) (e2 : Value.t > M) : M :=
LowM.bind e1 (fun v => match v with
 inl v => e2 v
 inr e => LowM.Pure (inr e)
end).
We define our semantics of a computation e
of type M
in simulations/proofs/CoqOfPython.v with the predicate:
{{ stack, heap  e ⇓ to_value  P_stack, P_heap }}
that we call a run or a trace, saying that:
stack
, heap
,e
terminates with a value,to_value
,P_stack
and P_heap
.Note that we do not explicit the resulting value and memory state of a computation in this predicate. We only say that it exists and verifies a few properties, that are here for compositionality. We have a purely functional function evaluate
that can derive the result of a run of a computation:
evaluate :
forall `{Heap.Trait} {A B : Set}
{stack : Stack.t} {heap : Heap} {e : LowM.t B}
{to_value : A > B} {P_stack : Stack.t > Prop} {P_heap : Heap > Prop}
(run : {{ stack, heap  e ⇓ to_value  P_stack, P_heap }}),
A * { stack : Stack.t  P_stack stack } * { heap : Heap  P_heap heap }
The function evaluate
is defined in Coq by a Fixpoint
. Its result is what we call a simulation, which is a purely functional definition equivalent to the orignal computation e
from Python. It is equivalent by construction.
A trace is an inductive in Set
that we can build with the following constructors:
Inductive t `{Heap.Trait} {A B : Set}
(stack : Stack.t) (heap : Heap)
(to_value : A > B) (P_stack : Stack.t > Prop) (P_heap : Heap > Prop) :
LowM.t B > Set :=
(* [Pure] primitive *)
 Pure
(result : A)
(result' : B) :
result' = to_value result >
P_stack stack >
P_heap heap >
{{ stack, heap 
LowM.Pure result' ⇓
to_value
 P_stack, P_heap }}
(* [StateRead] primitive *)
 CallPrimitiveStateRead
(mutable : Pointer.Mutable.t Value.t)
(object : Object.t Value.t)
(k : Object.t Value.t > LowM.t B) :
IsRead.t stack heap mutable object >
{{ stack, heap 
k object ⇓
to_value
 P_stack, P_heap }} >
{{ stack, heap 
LowM.CallPrimitive (Primitive.StateRead mutable) k ⇓
to_value
 P_stack, P_heap }}
(* [CallClosure] primitive *)
 CallClosure {C : Set}
(f : Value.t > Value.t > M)
(args kwargs : Value.t)
(to_value_inter : C > Value.t + Exception.t)
(P_stack_inter : Stack.t > Prop) (P_heap_inter : Heap > Prop)
(k : Value.t + Exception.t > LowM.t B) :
let closure := Data.Closure f in
{{ stack, heap 
f args kwargs ⇓
to_value_inter
 P_stack_inter, P_heap_inter }} >
(* We quantify over every possible values as we cannot compute the result of the closure here.
We only know that it exists and respects some constraints in this inductive definition. *)
(forall value_inter stack_inter heap_inter,
P_stack_inter stack_inter >
P_heap_inter heap_inter >
{{ stack_inter, heap_inter 
k (to_value_inter value_inter) ⇓
to_value
 P_stack, P_heap }}
) >
{{ stack, heap 
LowM.CallClosure closure args kwargs k ⇓
to_value
 P_stack, P_heap }}
(* ...cases for the other primitives of the monad... *)
In the Pure
case we return the final result of the computation. We check the state fulfills the predicate P_stack
and P_heap
, and that the result is the image by the function to_value
of some result
.
To read a value in memory, we rely on another predicate IsRead
that checks if the mutable
pointer is valid in the stack
or heap
and that the object
is the value at this pointer. We then call the continuation k
with this object. We have similar rules for allocating a new object in memory and writing at a pointer.
Note that we parameterize all our semantics by `{Heap.Trait}
that provides a specific Heap
type with read and write primitives. We can choose the implementation of the memory model that we want to use in our simulations in order to simplify the reasoning.
To call a closure, we first evaluate the closure with the arguments and keyword arguments. We then call the continuation k
with the result of the closure. We quantify over all possible results of the closure, as we cannot compute it here. This would require to be able to define Fixpoint
together with Inductive
, which is not possible in Coq. So we only know that the result of the closure exists, and can use the constraints on its result (the function to_value
and the predicates P_stack_inter
and P_heap_inter
) to build a run of the continuation.
The other constructors are not presented here but are similar to the above. We will also add a monadic primitive for loops with the following idea: we show that a loop terminates by building a trace, as traces are Inductive
so must be finite. We have no rules for the Impossible
case so that building the trace of a computation also shows that the Impossible
calls are in unreachable paths.
We have applied these technique to a small code example with allocation, memory read, and closure call primitives. We were able to show that the resulting simulation obtained by running evaluate
on the trace is equal to a simulation written by hand. The proof was just the tactic reflexivity
. We believe that we can automate most of the tactics used to build a run, except for the allocations were the user needs to make a choice (immediate, stack, or heap allocation, which address, ...).
To continue our experiments we now need to complete our semantics of Python, especially to take into account method and operator calls.
We have presented an alternative way to build simulations of imperative Python code in purely functional Coq code. The idea is to enable faster reasoning over Python code by removing the need to build explicit simulations. We plan to port this technique to other tools like coqofrust as well.
To see what we can do for you talk with us at contact@formal.land 🏇. For our previous projects, see our formal verification of the Tezos' L1!
]]>In this article, we will see how we specify the EVM in Coq by writing an interpreter that closely mimics the behavior of the Python code. We call that implementation a simulation as it aims to reproduce the behavior of the Python code, the reference.
In contrast to the automatic translation from Python, the simulation is a manual translation written in idiomatic Coq. We expect it to be ten times smaller in lines compared to the automatic translation, and of about the same size as the Python code. This is because the automatic translation needs to encode all the Python specific features in Coq, like variable mutations and the class system.
In the following article, we will show how we can prove that the simulation is correct, meaning that it behaves exactly as the automatic translation.
The code of this project is opensource and available on GitHub: formalland/coqofpython. This work follows a call from Vitalik Buterin for more formal verification of the Ethereum's code.
add
function 🧮We focus on a simulation for the add
function in vm/instructions/arithmetic.py that implements the addition primitive of the EVM. The Python code is:
def add(evm: Evm) > None:
"""
Adds the top two elements of the stack together, and pushes the result back
on the stack.
Parameters

evm :
The current EVM frame.
"""
# STACK
x = pop(evm.stack)
y = pop(evm.stack)
# GAS
charge_gas(evm, GAS_VERY_LOW)
# OPERATION
result = x.wrapping_add(y)
push(evm.stack, result)
# PROGRAM COUNTER
evm.pc += 1
Most of the functions of the interpreter are written in this style. They take the global state of the interpreter, called Evm
as input, and mutate it with the effect of the current instruction.
The Evm
structure is defined as:
@dataclass
class Evm:
"""The internal state of the virtual machine."""
pc: Uint
stack: List[U256]
memory: bytearray
code: Bytes
gas_left: Uint
env: Environment
valid_jump_destinations: Set[Uint]
logs: Tuple[Log, ...]
refund_counter: int
running: bool
message: Message
output: Bytes
accounts_to_delete: Set[Address]
touched_accounts: Set[Address]
return_data: Bytes
error: Optional[Exception]
accessed_addresses: Set[Address]
accessed_storage_keys: Set[Tuple[Address, Bytes32]]
It contains the current instruction pointer pc
, the stack of the EVM, the memory, the code, the gas left, ...
As the EVM is a stackbased machine, the addition function does the following:
x
and y
,result = x + y
,pc
.Note that all these operations might fail and raise an exception, for example,if the stack is empty when we pop x
and y
at the beginning.
The main sideeffects that we want to integrate into the Coq simulations are:
Evm
,For that, we use a state and error monad MS?
:
Module StateError.
Definition t (State Error A : Set) : Set :=
State > (A + Error) * State.
Definition return_ {State Error A : Set}
(value : A) :
t State Error A :=
fun state => (inl value, state).
Definition bind {State Error A B : Set}
(value : t State Error A)
(f : A > t State Error B) :
t State Error B :=
fun state =>
let (value, state) := value state in
match value with
 inl value => f value state
 inr error => (inr error, state)
end.
End StateError.
Notation "MS?" := StateError.t.
We parametrize it by an equivalent definition in Coq of the type Evm
and the type of exceptions that we might raise.
In Python the exceptions are a class that is extended as needed to add new kinds of exceptions. We use a closed sum type in Coq to represent the all possible exceptions that might happen in the EVM interpreter.
For the Evm
state, some functions might actually only modify a part of it. For example, the pop
function only modifies the stack
field. We use a mechanism of lens to specialize the state monad to only modify a part of the state. For example, the pop
function has the type:
pop : MS? (list U256.t) Exception.t U256.t
where list U256.t
is the type of the stack, while the add
function has type:
add : MS? Evm.t Exception.t unit
We define a lens for the stack in the Evm
type with:
Module Lens.
Record t (Big_A A : Set) : Set := {
read : Big_A > A;
write : Big_A > A > Big_A
}.
End Lens.
Module Evm.
Module Lens.
Definition stack : Lens.t Evm.t (list U256.t) := {
Lens.read := (* ... *);
Lens.write := (* ... *);
}.
We can then lift the pop
function to be used in a context where the Evm
state is modified with:
letS? x := StateError.lift_lens Evm.Lens.stack pop in
We keep in Coq all the type names from the Python source code. When a new class is created we create a new Coq type. When the class inherits from another one, we add a field in the Coq type to represent the parent class. Thus we work by composition rather than inheritance.
Here is an example of the primitive types defined in base_types.py:
class FixedUint(int):
MAX_VALUE: ClassVar["FixedUint"]
# ...
def __add__(self: T, right: int) > T:
# ...
class U256(FixedUint):
MAX_VALUE = 2**256  1
# ...
We simulate it by:
Module FixedUint.
Record t : Set := {
MAX_VALUE : Z;
value : Z;
}.
Definition __add__ (self right_ : t) : M? Exception.t t :=
(* ... *).
End FixedUint.
Module U256.
Inductive t : Set :=
 Make (value : FixedUint.t).
Definition of_Z (value : Z) : t :=
Make {
FixedUint.MAX_VALUE := 2^256  1;
FixedUint.value := value;
}.
(* ... *)
End U256.
For the imports, that are generally written with an explicit list of names:
from ethereum.base_types import U255_CEIL_VALUE, U256, U256_CEIL_VALUE, Uint
we follow the same pattern in Coq:
Require ethereum.simulations.base_types.
Definition U255_CEIL_VALUE := base_types.U255_CEIL_VALUE.
Module U256 := base_types.U256.
Definition U256_CEIL_VALUE := base_types.U256_CEIL_VALUE.
Module Uint := base_types.Uint.
This is a bit more verbose than the usual way in Coq to import a module, but it makes the translation more straightforward.
Finally, our Coq simulation of the add
function is the following:
Definition add : MS? Evm.t Exception.t unit :=
(* STACK *)
letS? x := StateError.lift_lens Evm.Lens.stack pop in
letS? y := StateError.lift_lens Evm.Lens.stack pop in
(* GAS *)
letS? _ := charge_gas GAS_VERY_LOW in
(* OPERATION *)
let result := U256.wrapping_add x y in
letS? _ := StateError.lift_lens Evm.Lens.stack (push result) in
(* PROGRAM COUNTER *)
letS? _ := StateError.lift_lens Evm.Lens.pc (fun pc =>
(inl tt, Uint.__add__ pc (Uint.Make 1))) in
returnS? tt.
We believe that it has a size and readability close to the original Python code. You can look at this definition in vm/instructions/simulations/arithmetic.v. As a reference, the automatic translation is 65 lines long and in vm/instructions/arithmetic.v.
We have seen how to write a simulation for one example of a Python function. We now need to do it for the rest of the code of the interpreter. We will also see in a following article how to prove that the simulation behaves as the automatic translation of the Python code in Coq.
For our formal verification services, reach us at contact@formal.land 🏇! To know more about what we have done, see our previous project on the verification of the L1 of Tezos.
]]>We want to import specifications written in Python to a formal system like Coq. In particular, we are interested in the reference specification of Ethereum, which describes how EVM smart contracts run. Then, we will be able to use this specification to either formally verify the various implementations of the EVM or smart contracts.
All this effort follows a Tweet from Vitalik Buterin hoping for more formal verification of the Ethereum's code:
One application of AI that I am excited about is AIassisted formal verification of code and bug finding.
Right now ethereum's biggest technical risk probably is bugs in code, and anything that could significantly change the game on that would be amazing.
— Vitalik Buterin
We will now describe the technical development of coqofpython
. For the curious, all the code is on GitHub: formalland/coqofpython.
A first step we need to do to translate Python code is to read it in a programmatic way. For simplicity and better integration, we chose to write coqofpython
in Python.
We use the ast module to parse the code and get an abstract syntax tree (AST) of the code. This is a tree representation of the code that we can manipulate in Python. We could have used other representations, such as the Python bytecode, but it seemed too lowlevel to be understandable by a human.
Given the path to a Python file, we get its AST with the following code:
import ast
def read_python_file(path: str) > ast.Module:
with open(path, "r") as file:
return ast.parse(file.read())
This code is very short, and we benefit from the general elegance of Python. There is no typing or advanced data types in Python, keeping the AST rather small. Here is an extract of it:
expr = BoolOp(boolop op, expr* values)
 NamedExpr(expr target, expr value)
 BinOp(expr left, operator op, expr right)
 UnaryOp(unaryop op, expr operand)
 Lambda(arguments args, expr body)
 IfExp(expr test, expr body, expr orelse)
 Dict(expr* keys, expr* values)
 Set(expr* elts)
 ListComp(expr elt, comprehension* generators)
 SetComp(expr elt, comprehension* generators)
 ... more cases ...
An expression is described as being of one of several kinds. For example, the application of a binary operator such as:
1 + 2
corresponds to the case BinOp
with 1
as the left
expression, +
as the op
operator, and 2
as the right
expression.
We translate each element of the Python's AST into a string of Coq code. We keep track of the current indentation level in order to present a nice output. Here is the code to translate the binary operator expressions:
def generate_expr(indent, is_with_paren, node: ast.expr):
if isinstance(node, ast.BoolOp):
...
elif isinstance(node, ast.BinOp):
return paren(
is_with_paren,
generate_operator(node.op) + " (\n" +
generate_indent(indent + 1) +
generate_expr(indent + 1, False, node.left) + ",\n" +
generate_indent(indent + 1) +
generate_expr(indent + 1, False, node.right) + "\n" +
generate_indent(indent) + ")"
)
elif ...
We have the current number of indentation levels in the indent
variable. We use the flag is_with_paren
to know whether we should add parenthesis around the current expression if it is the subexpression of another one.
We apply the node.op
operator on the two parameters node.left
and node.right
. For example, the translation of the Python code 1 + 2
will be:
BinOp.add (
Constant.int 1,
Constant.int 2
)
We use a special notation f ( x1, ..., xn )
to represent a function application in a monadic context. In the next section, we explain why we need this notation.
One of the difficulties in translating some code to a language such as Coq is that Coq is purely functional. This means that a function can never modify a variable or raise an exception. The nonpurely functional actions are called sideeffects.
To solve this issue, we represent the sideeffects of the Python code in a monad in Coq. A monad is a special data structure representing the sideeffects of a computation. We can chain monadic actions together to represent a sequence of sideeffects.
We thus have two Coq types:
Value.t
for the Python values (there is only one type for all values, as Python is a dynamically typed language),M
for the monadic expressions.Note that we do not need to parametrize the monad by the type of the values, as we only have one type of value.
According to the reference manual of Python on the data model:
All data in a Python program is represented by objects or by relations between objects.
Every object has an identity, a type and a value. An object’s identity never changes once it has been created; you may think of it as the object’s address in memory.
Like its identity, an object’s type is also unchangeable.
The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable.
By following this description, we propose this formalization for the values:
Module Data.
Inductive t (Value : Set) : Set :=
 Ellipsis
 Bool (b : bool)
 Integer (z : Z)
 Tuple (items : list Value)
(* ... various other primitive types like lists, ... *)
 Closure {Value M : Set} (f : Value > Value > M)
 Klass {Value M : Set}
(bases : list (string * string))
(class_methods : list (string * (Value > Value > M)))
(methods : list (string * (Value > Value > M))).
End Data.
Module Object.
Record t {Value : Set} : Set := {
internal : option (Data.t Value);
fields : list (string * Value);
}.
End Object.
Module Pointer.
Inductive t (Value : Set) : Set :=
 Imm (data : Object.t Value)
 Mutable {Address A : Set}
(address : Address)
(to_object : A > Object.t Value).
End Pointer.
Module Value.
Inductive t : Set :=
 Make (globals : string) (klass : string) (value : Pointer.t t).
End Value.
We describe a Value.t
by:
klass
and a module name globals
from which the class is defined,A Pointer.t
is either an immutable object Imm
or a mutable object Mutable
with an address and a function to get the object from what is stored in the memory. This function to_object
is required as we plan to allow the user to provide its own custom memory model.
An Object.t
has a list of named fields that we can populate in the __init__
method of a class. It also has a special internal
field that we can use to store special kinds of data, like primitive values.
In Data.t
, we list the various primitive values that we use to define the primitive types of the Python language. We have:
*args
and **kwargs
and returns a monadic value,For now, we axiomatize the monad M
:
Parameter M : Set.
We will see later how to define it, probably by taking some inspiration from our monad from our similar project coqofrust.
To make the monadic code less heavy, we use a notation inspired by the async/await
notation of many languages. We believe it to be less heavy than the monadic notation of languages like Haskell. We note:
f ( x1, ..., xn )
to call a function f
of type:
Value.t > ... > Value.t > M
with the arguments x1
, ..., xn
of type Value.t
and binds its result to the current continuation in the context of the tactic ltac:(M.monadic ...)
. See our blog post Monadic notation for the Rust translation for more information.
In summary:
f ( x1, ..., xn )
is like await
,ltac:(M.monadic ...)
is like async
.Now we talk about how we handle the variable names and link them to their definitions. In the reference manual of Python, the part Execution model gives some information.
For now, we distinguish between two scopes, the global one (toplevel definitions) and the local one for variables defined in a function. We might introduce a stack of local scopes to handle nested functions.
We name the global scope with a string, that is the path of the current file. Having absolute names helps us translating each file independently. The only file that a translated file requires is CoqOfPython.CoqOfPython
, to have the definition of the values and the monad.
To translate import
statements, we use assertions:
Axiom ethereum_crypto_imports_elliptic_curve :
IsImported globals "ethereum.crypto" "elliptic_curve".
Axiom ethereum_crypto_imports_finite_field :
IsImported globals "ethereum.crypto" "finite_field".
This represents:
from . import elliptic_curve, finite_field
It means that in the current global scope globals
we can use the name "elliptic_curve"
from the other global scope "ethereum.crypto"
.
We set the local scope at the entry of a function with the call:
M.set_locals ( args, kwargs, [ "x1"; ...; "xn" ] )
for a function whose parameter names are x1
, ..., xn
. For uniformity, we always group the function's parameters as *args
and **kwargs
. We do not yet handle the default values.
When a user creates or updates a local variable x
with a value value
, we run:
M.assign_local "x" value : M
To read a variable, we have a primitive:
M.get_name : string > string > M
It takes as a parameter the name of the current global scope and the name of the variable the are reading. The local scope should be accessible from the monad. For now all these primitives are axiomatized.
The code base that we analyze, the Python specification of Ethereum, contains 28,455 lines of Python, excluding comments. When we translate it to Coq we obtain 299,484 lines. This is a roughly ten times increase.
The generated code completely compiles. For now, we avoid some complex Python expressions, like list comprehension, by generating a dummy expression instead. Having all the code that compiles will allow us to iterate and add support for more Python features with a simple check: making sure that all the code still compiles.
As an example, we translate the following function:
def bnf2_to_bnf12(x: BNF2) > BNF12:
"""
Lift a field element in `BNF2` to `BNF12`.
"""
return BNF12.from_int(x[0]) + BNF12.from_int(x[1]) * (
BNF12.i_plus_9  BNF12.from_int(9)
)
to the Coq code:
Definition bnf2_to_bnf12 : Value.t > Value.t > M :=
fun (args kwargs : Value.t) => ltac:(M.monadic (
let _ := M.set_locals ( args, kwargs, [ "x" ] ) in
let _ := Constant.str "
Lift a field element in `BNF2` to `BNF12`.
" in
let _ := M.return_ (
BinOp.add (
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
M.get_subscript (
M.get_name ( globals, "x" ),
Constant.int 0
)
],
make_dict []
),
BinOp.mult (
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
M.get_subscript (
M.get_name ( globals, "x" ),
Constant.int 1
)
],
make_dict []
),
BinOp.sub (
M.get_field ( M.get_name ( globals, "BNF12" ), "i_plus_9" ),
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
Constant.int 9
],
make_dict []
)
)
)
)
) in
M.pure Constant.None_)).
We continue working on the translation from Python to Coq, especially to now add a semantics to the translation. Our next goal is to have a version, written in idiomatic Coq, of the file src/ethereum/paris/vm/instructions/arithmetic.py, and proven equal to the original code. This will open the door to making a Coq specification of the EVM that is always synchronized to the Python's version.
For our services, reach us at contact@formal.land 🏇! We want to ensure the blockchain's L1 and L2 are bugfree, thanks to a mathematical analysis of the code. See our previous project on the L1 of Tezos.
]]>To solve this issue, we worked on the translation of the core and alloc crates of Rust using coqofrust
. These are very large code bases, with a lot of unsafe or advanced Rust code. We present what we did to have a "best effort" translation of these crates. The resulting translation is in the following folders:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat ☎️!
An initial run of coqofrust
on the alloc
and core
crates of Rust generated us two files of a few hundred thousands lines of Coq corresponding to the whole translation of these crates. This is a first good news, as it means the tool runs of these large code bases. However the generated Coq code does not compile, even if the errors are very rare (one every few thousands lines).
To get an idea, here is the size of the input Rust code as given by the cloc
command:
alloc
: 26,299 lines of Rust codecore
: 54,192 lines of Rust codeGiven that this code uses macros that we expand in our translation, the actual size that we have to translate is even bigger.
The main change we made was to split the output generated by coqofrust
with one file for each input Rust file. This is possible because our translation is insensitive to the order of definitions and contextfree. So, even if there are typically cyclic dependencies between the files in Rust, something that is forbidden in Coq, we can still split them.
We get the following sizes as output:
alloc
: 54 Coq files, 171,783 lines of Coq codecore
: 190 Coq files, 592,065 lines of Coq codeThe advantages of having the code split are:
We had some bugs related to the collisions between module names. These can occur when we choose a name for the module for an impl
block. We fixed these by adding more information in the module names to make them more unique, like the where
clauses that were missing. For example, for the implementation of the Default
trait for the Mapping
type:
#[derive(Default)]
struct Mapping<K, V> {
// ...
}
we were generating the following Coq code:
Module Impl_core_default_Default_for_dns_Mapping_K_V.
(* ...trait implementation ... *)
End Impl_core_default_Default_for_dns_Mapping_K_V.
We now generate:
Module Impl_core_default_Default_where_core_default_Default_K_where_core_default_Default_V_for_dns_Mapping_K_V.
(* ... *)
with a module name that includes the where
clauses of the impl
block, stating that both K
and V
should implement the Default
trait.
Here is the list of files that do not compile in Coq, as of today:
alloc/boxed.v
core/any.v
core/array/mod.v
core/cmp/bytewise.v
core/error.v
core/escape.v
core/iter/adapters/flatten.v
core/net/ip_addr.v
This represents 4% of the files. Note that in the files that compile there are some unhandled Rust constructs that are axiomatized, so this does not give the whole picture of what we do not support.
Here is the source code of the unwrap_or_default
method for the Option
type:
pub fn unwrap_or_default(self) > T
where
T: Default,
{
match self {
Some(x) => x,
None => T::default(),
}
}
We translate it to:
Definition unwrap_or_default (T : Ty.t) (τ : list Ty.t) (α : list Value.t) : M :=
let Self : Ty.t := Self T in
match τ, α with
 [], [ self ] =>
ltac:(M.monadic
(let self := M.alloc ( self ) in
M.read (
M.match_operator (
self,
[
fun γ =>
ltac:(M.monadic
(let γ0_0 :=
M.get_struct_tuple_field_or_break_match (
γ,
"core::option::Option::Some",
0
) in
let x := M.copy ( γ0_0 ) in
x));
fun γ =>
ltac:(M.monadic
(M.alloc (
M.call_closure (
M.get_trait_method ( "core::default::Default", T, [], "default", [] ),
[]
)
)))
]
)
)))
 _, _ => M.impossible
end.
We prove that it is equivalent to the simpler functional code:
Definition unwrap_or_default {T : Set}
{_ : core.simulations.default.Default.Trait T}
(self : Self T) :
T :=
match self with
 None => core.simulations.default.Default.default (Self := T)
 Some x => x
end.
This simpler definition is what we use when verifying code. The proof of equivalence is in CoqOfRust/core/proofs/option.v. In case the original source code changes, we are sure to capture these changes thanks to our proof. Because the translation of the core
library was done automatically, we trust the generated definitions more than definitions that would be done by hand. However, there can still be mistakes or incompleteness in coqofrust
, so we still need to check at proof time that the code makes sense.
We can now work on the verification of Rust programs with more trust in our formalization of the standard library. Our next target is to simplify our proof process, which is still tedious. In particular, showing that simulations are equivalent to the original Rust code requires doing the name resolution, introduction of highlevel types, and removal of the sideeffects. We would like to split these steps.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land 💌! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>One of the challenges of our translation from Rust to Coq is that the generated code is very verbose. The size increase is about ten folds in our examples. A reasons is that we use a monad to represent side effects in Coq, so we need to name each intermediate result and apply the bind
operator. Here, we will present a monadic notation that prevents naming intermediate results to make the code more readable.
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat ☎️!
Here is the Rust source code that we consider:
fn add(a: i32, b: i32) > i32 {
a + b
}
Before, we were generating the following Coq code, with let*
as the notation for the bind:
Definition add (τ : list Ty.t) (α : list Value.t) : M :=
match τ, α with
 [], [ a; b ] =>
let* a := M.alloc a in
let* b := M.alloc b in
let* α0 := M.read a in
let* α1 := M.read b in
BinOp.Panic.add α0 α1
 _, _ => M.impossible
end.
Now, with the new monadic notation, we generate:
Definition add (τ : list Ty.t) (α : list Value.t) : M :=
match τ, α with
 [], [ a; b ] =>
ltac:(M.monadic
(let a := M.alloc ( a ) in
let b := M.alloc ( b ) in
BinOp.Panic.add ( M.read ( a ), M.read ( b ) )))
 _, _ => M.impossible
end.
The main change is that we do not need to introduce intermediate let*
expressions with generated names. The code structure is more similar to the original Rust code, with additional calls to memory primitives such as M.alloc
and M.read
.
The notation f ( x1, ..., xn )
represents the call to the function f
with the arguments x1
, ..., xn
returning a monadic result. We bind the result with the current continuation that goes up to the wrapping ltac:(M.monadic ...)
tactic. We automatically transform the let
into a let*
with the M.monadic
tactic when needed.
We use this notation in all the function bodies that we generate, that are all in a monad to represent side effects. We call the ltac:(M.monadic ...)
tactic at the start of the functions, as well as at the start of closure bodies that are defined inside functions. This also applies to the translation of if
, match
, and loop
expressions, as we represent their bodies as functions.
Here is an example of code with a match
expression:
fn add(a: i32, b: i32) > i32 {
match a  b {
0 => a + b,
_ => a  b,
}
}
We translate it to:
Definition add (τ : list Ty.t) (α : list Value.t) : M :=
match τ, α with
 [], [ a; b ] =>
ltac:(M.monadic
(let a := M.alloc ( a ) in
let b := M.alloc ( b ) in
M.read (
M.match_operator (
M.alloc ( BinOp.Panic.sub ( M.read ( a ), M.read ( b ) ) ),
[
fun γ =>
ltac:(M.monadic
(let _ :=
M.is_constant_or_break_match (
M.read ( γ ),
Value.Integer Integer.I32 0
) in
M.alloc (
BinOp.Panic.add ( M.read ( a ), M.read ( b ) )
)));
fun γ =>
ltac:(M.monadic (
M.alloc (
BinOp.Panic.sub ( M.read ( a ), M.read ( b ) )
)
))
]
)
)))
 _, _ => M.impossible
end.
We see that we call the tactic M.monadic
for each branch of the match
expression.
The M.monadic
tactic is defined in M.v. The main part is:
Ltac monadic e :=
lazymatch e with
(* ... *)
 context ctxt [M.run ?x] =>
lazymatch context ctxt [M.run x] with
 M.run x => monadic x
 _ =>
refine (M.bind _ _);
[ monadic x
 let v := fresh "v" in
intro v;
let y := context ctxt [v] in
monadic y
]
end
(* ... *)
end.
In our translation of Rust, all of the values have the common type Value.t
. The monadic bind is of type M > (Value.t > M) > M
where M
is the type of the monad. The M.run
function is an axiom that we use as a marker to know where we need to apply M.bind
. The type of M.run
is:
Axiom run : M > Value.t.
The notation for monadic function calls is defined using the M.run
axiom with:
Notation "e ( e1 , .. , en )" := (M.run ((.. (e e1) ..) en)).
When we encounter a M.run
(line 4) we apply the M.bind
(line 8) to the monadic expression x
(line 9) and its continuation ctx
that we obtain thanks to the context
keyword (line 4) of the matching of expressions in Ltac.
There is another case in the M.monadic
tactic to handle the let
expressions, that is not shown here.
Thanks to this new monadic notation, the generated Coq code is more readable and closer to the original Rust code. This should simplify our work in writing proofs on the generated code, as well as debugging the translation.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land 💌! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>coqofrust
to:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat ☎️!
dns
example 🚀We continue with our previous example dns.rs, which is composed of around 200 lines of Rust code.
The next error that we encounter when typechecking the Coq translation of dns.rs
is:
File "./examples/default/examples/ink_contracts/dns.v", line 233, characters 2227:
Error: The reference deref was not found in the current environment.
In Rust, we can either take the address of a value with &
, or dereference a reference with *
. In our translation, we do not distinguish between the four following pointer types:
&
&mut
*const
*mut
We let the user handle these in different ways if it can simplify their proofs, especially regarding the distinction between mutable and nonmutable pointers. It simplifies the definition of our borrowing and dereferencing operators, as we need only two to cover all cases. We even go further: we remove these two operators in the translation, as they are the identity in our case!
To better understand why they are the identity, we need to see that there are two kinds of Rust values in our representation:
The value itself is useful to compute over the values. For example, we use it to define the primitive addition over integers. The value with its address corresponds to the final Rust expression. Indeed, we can take the address of any subexpression in Rust with the &
operator, so each subexpression should come with its address. When we take the address of an expression, we:
Thus, the &
operator behaves as the identity function followed by an allocation. Similarly, the *
is a memory read followed by the identity function. Since we already use the alloc and read operations to go from a value to a value with its address and the other way around, we do not need to define the *
and &
operators in our translation and remove them.
We now need to distinguish between the function calls, that use the primitive:
M.get_function : string > M
to find the right function to call when defining the semantics of the program (even if the function is defined later), and the calls to primitive operators (+
, *
, !
, ...) that we define in our base library for Rust in Coq. The full list of primitive operators is given by:
We adapted the handling of primitive operators from the code we had before and added a few other fixes so that now the dns.rs
example typechecks in Coq 🎊! We will now focus on fixing the other examples.
But let us first clean the code a bit. All the expressions in the internal AST of coqofrust
are in a wrapper with the current type of the expression:
pub(crate) struct Expr {
pub(crate) kind: Rc<ExprKind>,
pub(crate) ty: Option<Rc<CoqType>>,
}
pub(crate) enum ExprKind {
Pure(Rc<Expr>),
LocalVar(String),
Var(Path),
Constructor(Path),
// ... all the cases
Having access to the type of each subexpression was useful before annotating the let
expressions. This is not required anymore, as all the values have the type Value.t
. Thus, we remove the wrapper Expr
and rename ExprKind
into Expr
. The resulting code is easier to read, as wrapping everything with a type was verbose sometimes.
We also cleaned some translated types that were not used anymore in the code, removed unused Derive
traits, and removed the monadic translation on the types.
To handle the remaining examples of our test suite (extracted from the snippets of the Rust by Example book), we mainly needed to reimplement the pattern matching on the new untyped values. Here is an example of Rust code with matching:
fn matching(tuple: (i32, i32)) > i32 {
match tuple {
(0, 0) => 0,
(_, _) => 1,
}
}
with its translation in Coq:
Definition matching (𝜏 : list Ty.t) (α : list Value.t) : M :=
match 𝜏, α with
 [], [ tuple ] =>
let* tuple := M.alloc tuple in
let* α0 :=
match_operator
tuple
[
fun γ =>
let* γ0_0 := M.get_tuple_field γ 0 in
let* γ0_1 := M.get_tuple_field γ 1 in
let* _ :=
let* α0 := M.read γ0_0 in
M.is_constant_or_break_match α0 (Value.Integer Integer.I32 0) in
let* _ :=
let* α0 := M.read γ0_1 in
M.is_constant_or_break_match α0 (Value.Integer Integer.I32 0) in
M.alloc (Value.Integer Integer.I32 0);
fun γ =>
let* γ0_0 := M.get_tuple_field γ 0 in
let* γ0_1 := M.get_tuple_field γ 1 in
M.alloc (Value.Integer Integer.I32 1)
] in
M.read α0
 _, _ => M.impossible
end.
Here is a breakdown of how it works:
match_operator
primitive that takes a value to match on, tuple
, and a list of functions that try to match the value with a pattern and execute some code in case of success. We execute the matching functions successively until one succeeds and we stop. There should be at least one succeeding function as patternmatch in Rust is exhaustive.γ
that is the address of the tuple tuple
given as parameter to the function. Having the address might be required for some operations, like doing subsequent matching by reference or using the &
operator in the match
's body.γ
are generated to avoid name clashes. They correspond to the depth of the subpattern being considered, followed by the index of the current item in this subpattern.0
. We use the M.is_constant_or_break_match
primitive that checks if the value is a constant and if it is equal to the expected value. If it is not the case, it exits the current matching function, and the match_operator
primitive will evaluate the next one, going to line 19.M.alloc
followed by M.read
to return the result. This could be simplified, as immediately reading an allocated value is like running the identity function.By implementing the new version of the patternmatching, as well as a few other smaller fixes, we were able to make all the examples typecheck again! We now need to fix the proofs we had on the erc20.v example, as the generated code changed a lot.
Unfortunately, all these changes in the generated code are breaking our proofs. We still want to write our specifications and proofs by first showing a simulation of the Rust code with a simpler and functional definition. Before, with our simulations, we were:
Now we also have to:
We have not finished updating the proofs but still merged our work in main
with the pull request #472 as this was taking too long. The proof that we want to update is in the file proofs/erc20.v and is about the smart contract erc20.rs.
Our basic strategy for the proof, in order to handle the untyped Rust values of the new translation, is to define various φ
operators coming from a userdefined Coq type to a Rust value of type Value.t
. These translate the data types that we define to represent the Rust types of the original program. Note that we previously had trouble translating the Rust types in the general case, especially for mutually recursive types or types involving a lot of trait manipulations.
More formally, we introduce the Coq typeclass:
Class ToValue (A : Set) : Set := {
Φ : Ty.t;
φ : A > Value.t;
}.
Arguments Φ _ {_}.
This describes how to go from a userdefined type in Coq to the equivalent representation in Value.t
. In addition to the φ
operator, we also define the Φ
operator that gives the Rust type of the Coq type. This type is required to give for polymorphic definitions.
We always go from userdefined types to Value.t
. We write our simulation statements like this:
{{env, state 
code.example.get_at_index [] [φ vector; φ index] ⇓
inl (φ (simulations.example.get_at_index vector index))
 state'}}
where:
{{env, state  rust_program ⇓ simulation_result  state'}}
is our predicate to state an evaluation of a Rust program to a simulation result. We apply the φ
operator to the arguments of the Rust program and to the result of the simulation. In some proofs, we set this operator as Opaque
in order to keep track of it and avoid unwanted reductions.
The trait definitions, as well as trait constraints, are absent from the generated Coq code. For now, we add them back as follows, for the example of the Default
trait:
We define a Default
typeclass in Coq:
Module Default.
Class Trait (Self : Set) : Set := {
default : Self;
}.
End Default.
We define what it means to implement the Default
trait and have a corresponding simulation:
Module Default.
Record TraitHasRun (Self : Set)
`{ToValue Self}
`{core.simulations.default.Default.Trait Self} :
Prop := {
default :
exists default,
IsTraitMethod
"core::default::Default" (Φ Self) []
"default" default /\
Run.pure
(default [] [])
(inl (φ core.simulations.default.Default.default));
}.
End Default.
where Run.pure
is our simulation predicate for the case where the state
does not change.
Finally, we use the TraitHasRun
predicate as an additional hypothesis for simulation proofs on functions that depend on the Default
trait in Rust:
(** Simulation proof for `unwrap_or_default` on the type `Option`. *)
Lemma run_unwrap_or_default {T : Set}
{_ : ToValue T}
{_ : core.simulations.default.Default.Trait T}
(self : option T) :
core.proofs.default.Default.TraitHasRun T >
Run.pure
(core.option.Impl_Option_T.unwrap_or_default (Φ T) [] [φ self])
(inl (φ (core.simulations.option.Impl_Option_T.unwrap_or_default self))).
Proof.
(* ... *)
Qed.
We still have a lot to do, especially in finding the right approach to verify the newly generated Rust code. But we have finalized our new translation mode without types and ordering, which helps to successfully translate many more Rust examples. We also do not need to translate the dependencies of a project anymore before compiling it.
Our next target is to translate the whole of Rust's standard library (with the help of some axioms for the expressions which we do not handle yet), in order to have a faithful definition of the Rust primitives, such as functions of the option and vec modules.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land 💌! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>coqofrust
to make the generated code use these new definitions.
With this new translation strategy, to support more Rust code, we want:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat!
We implemented the new monad and the type Value.t
holding any kind of Rust values as described in the previous blog post. For now, we have removed the definitions related to the standard library of Rust (everything except the base definitions such as the integer types). This should not be an issue to typecheck the generated Coq code, as the new code should be independent of the ordering of definitions: in particular, it should typecheck even if the needed definitions are not yet there.
We added some definitions for the primitive unary and binary operators. These include some operations on the integers such arithmetic operations (with or without overflow, depending on the compilation mode), as well as comparisons (equality, lesser or equal than, ...).
Now that the main library file CoqOfRust/CoqOfRust.v compiles in Coq, we can start to test the translation on our examples.
We generate new snapshots for our translations with:
cargo build && time python run_tests.py
This builds the project coqofrust
(with a lot of warning about unused code for now) and regenerates our snapshots: for each Rust file in the examples directory, we generate a Coq file with the same name but the extension .v
. We generate two versions:
We first try to typecheck and fix the code generated in axiom mode.
We have a first error for type aliases that we do not translate properly. We need access to the fully qualified name of the alias. We do that by combining calls to the functions:
As a result, for the file examples/ink_contracts/basic_contract_caller.rs, we translate the type alias:
type Hash = [u8; 32];
into the Coq code:
Axiom Hash :
(Ty.path "basic_contract_caller::Hash") =
(Ty.apply (Ty.path "array") [Ty.path "u8"]).
Then, during the proofs, we will be able to substitute the type Hash
by its definition when it appears. Note that we now translate types by values of the type Ty.t
, so there should be no difficulties in rewriting types.
We should add the length of the array in the type. This is not done yet.
In axiom mode, we remove most of the trait definitions. Instead, with our new translation model, the traits are mostly unique names (the absolute path of the trait definition). The main use of traits is to distinguish them from other traits, to know which trait implementation to use when calling a trait's method. We still translate the provided methods (that are default methods in the trait definition) to axioms and add a predicate stating that they are associated with the current trait. For example, we translate the following Rust trait:
// crate `my_crate`
trait Animal {
fn new(name: &'static str) > Self;
fn name(&self) > &'static str;
fn noise(&self) > &'static str;
fn talk(&self) {
println!("{} says {}", self.name(), self.noise());
}
}
to the Coq code:
(* Trait *)
Module Animal.
Parameter talk : (list Ty.t) > (list Value.t) > M.
Axiom ProvidedMethod_talk : M.IsProvidedMethod "my_crate::Animal" talk.
End Animal.
We realize with this example that the translation in axiom mode generates very few errors, as we remove all the type definitions and all the function axioms have the same signature:
(* A list of types that can be empty for nonpolymorphic functions,
a list of parameters, and a return value in the monad `M`. *)
list Ty.t > list Value.t > M
so the typechecking of these axioms never fails. We thus jump to the full definition mode as this is where our new approach might fail.
We now try to typecheck the generated Coq code in full definition mode. We start with the dns.rs smart contract example.
This example is interesting, as it contains polymorphic implementations, such as for the mock type Mapping
:
#[derive(Default)]
struct Mapping<K, V> {
_key: core::marker::PhantomData<K>,
_value: core::marker::PhantomData<V>,
}
that implements the Default trait on the type Mapping<K, V>
for two type parameters K
and V
. We translate it to:
(* Struct Mapping *)
Module Impl_core_default_Default_for_dns_Mapping_K_V.
(*
Default
*)
Definition default (𝜏 : list Ty.t) (α : list Value.t) : M :=
match 𝜏, α with
 [ Self; K; V ], [] =>
let* α0 :=
M.get_method
"core::default::Default"
"default"
[ (* Self *) Ty.apply (Ty.path "core::marker::PhantomData") [ K ] ] in
let* α1 := M.call α0 [] in
let* α2 :=
M.get_method
"core::default::Default"
"default"
[ (* Self *) Ty.apply (Ty.path "core::marker::PhantomData") [ V ] ] in
let* α3 := M.call α2 [] in
M.pure
(Value.StructRecord "dns::Mapping" [ ("_key", α1); ("_value", α3) ])
 _, _ => M.impossible
end.
Axiom Implements :
forall (K V : Ty.t),
M.IsTraitInstance
"core::default::Default"
(* Self *) (Ty.apply (Ty.path "dns::Mapping") [ K; V ])
[]
[ ("default", InstanceField.Method default) ]
[ K; V ].
End Impl_core_default_Default_for_dns_Mapping_K_V.
Here are the interesting bits of this code:
On line 1, we translate the Mapping
type into a single comment, as the types disappear in our translation and become just markers. The marker for Mapping
is its absolute name Ty.path "dns::Mapping"
.
On line 7, the function default
takes a list of types 𝜏
as a parameter in case it is polymorphic. Here, this method is not polymorphic, but we still add the 𝜏
parameter for uniformity. We also take three additional type parameters:
Self
K
V
that represent the Self
type on which the trait is implemented, and the two type parameters of the Mapping
type. These will be provided when calling the default
method.
On line 11, we use the primitive M.get_method
(axiomatized for now) to get the method default
of the trait core::default::Default
for the type core::marker::PhantomData<K>
. Here, we see that having access to the type K
in the body of the default
function is useful, as it helps us to disambiguate between the various implementations of the Default
trait instances that we call. Here, we provide the Self
type of the trait in a list of a single element. If the Default
trait or the default
method were polymorphic, we would also append these type parameters in this list.
On line 15, we call the default
method instance that we found with an empty list of arguments.
On line 23, we build a value of type Mapping
with the two fields _key
and _value
initialized with the results of the two calls to the default
method. We use the Value.StructRecord
constructor to build the value, and its result is of type Value.t
like all other Rust values.
On line 24, we eliminate a case with a wrong number of type and value arguments. This should never happen as the arity of all the function calls is checked by the Rust typechecker.
On line 27, we state that we have a new instance of the Default
trait for the Mapping
type, with the default
method implemented by the default
function. This is true for any values of the types K
and V
.
On line 34, we specify that [K, V]
are the type parameters of this implementation that should be given as extra parameters when calling the default
method of this instance, together with the Self
type.
Next, we have a polymorphic implementation of mock associated functions for the Mapping
type:
impl<K, V> Mapping<K, V> {
fn contains(&self, _key: &K) > bool {
unimplemented!()
}
// ...
We translate it to:
Module Impl_dns_Mapping_K_V.
Definition Self (K V : Ty.t) : Ty.t :=
Ty.apply (Ty.path "dns::Mapping") [ K; V ].
(*
fn contains(&self, _key: &K) > bool {
unimplemented!()
}
*)
Definition contains (𝜏 : list Ty.t) (α : list Value.t) : M :=
match 𝜏, α with
 [ Self; K; V ], [ self; _key ] =>
let* self := M.alloc self in
let* _key := M.alloc _key in
let* α0 := M.var "core::panicking::panic" in
let* α1 := M.read (mk_str "not implemented") in
let* α2 := M.call α0 [ α1 ] in
never_to_any α2
 _, _ => M.impossible
end.
Axiom AssociatedFunction_contains :
forall (K V : Ty.t),
M.IsAssociatedFunction (Self K V) "contains" contains [ K; V ].
(* ... *)
We follow a similar approach as for the translation of trait implementations, especially regarding the handling of polymorphic type variables. Here are some differences:
Self
type as a function of the type parameters K
and V
. This is useful for avoiding repeating the same type expression later.M.IsAssociatedFunction
to state that we have a new associated function contains
for the Mapping
type, with the contains
method implemented by the contains
function. This is true for any values of the types K
and V
. Like for the trait implementations, we explicit the list [K, V]
that will be given as an extra parameter to the function contains
.In the next blog post, we will see how we continue to translate the examples in full definition mode. There is still a lot to do to get to the same level of Rust support as before, but we are hopeful that our new approach will be more robust and easier to maintain.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land! Formal verification provides the highest level of safety for critical applications. See the White House report on secure software development for more on the importance of formal verification.
]]>coqofrust
still has some limitations:
We will present how we plan to improve our tool to address these limitations.
As emphasized in the recent report from the White House, memory safety and formal verification are keys to ensure secure and correct software. Rust provides memory safety and we provide formal verification on top of it with coqofrust
.
We will take the Rust serde serialization library to have an example of code to translate in Coq. This is a popular Rust library that is used in almost all projects, either as a direct or transitive dependency. Serialization has a simple specification (being a bijection between the data and its serialized form) and is a good candidate for formal verification. We might verify this library afterwards if there is a need.
This work is funded by the Aleph Zero cryptocurrency in order to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase totally bugfree. Contact us at contact@formal.land to chat! We offer a free audit to assess the feasibility of formal verification on your case.
Our company goal is to make formal verification accessible to all projects, reducing its cost to 20% of the development cost. There should be no reason to have bugs in enduser products!
We start by running the command:
cargo coqofrust
in the serde
directory. We get a lot of warnings, but the translation does not panic as it tries to always produce something for debugging purposes. We have two kinds of warnings.
The warning is the following:
warning: Constants in patterns are not yet supported.
> serde/src/de/mod.rs:2277:13

2277  0 => panic!(), // special case elsewhere
 ^
The reason why we did not handle constants in patterns is that they are represented in a special format in the Rust compiler that was not obvious to handle. The definition of rustc_middle::mir::consts::Const representing the constants in patterns is:
pub enum Const<'tcx> {
Ty(Const<'tcx>),
Unevaluated(UnevaluatedConst<'tcx>, Ty<'tcx>),
Val(ConstValue<'tcx>, Ty<'tcx>),
}
There are three cases, and each contains several more cases. To fix this issue, we added the code to handle the signed and unsigned integers, which are enough for our serde
example. We will need to add other cases later, especially for the strings. This allowed us to discover and fix a bug in our handling of patterns for tuples with elision ..
, like in the example:
fn main() {
let triple = (0, 2, 3);
match triple {
(0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
(1, ..) => println!("First is `1` and the rest doesn't matter"),
(.., 2) => println!("last is `2` and the rest doesn't matter"),
(3, .., 4) => println!("First is `3`, last is `4`, and the rest doesn't matter"),
_ => println!("It doesn't matter what they are"),
}
}
These changes are in the pullrequest coqofrust#470.
parent_kind
We get a second form of warning:
unimplemented parent_kind: Struct
expression: Expr {
kind: ZstLiteral {
user_ty: None,
},
ty: FnDef(
DefId(2:31137 ~ core[10bc]::cmp::Reverse::{constructor#0}),
[
T/#1,
],
),
temp_lifetime: Some(
Node(14),
),
span: serde/src/de/impls.rs:778:22: 778:29 (#0),
}
This is for some cases of expressions rustc_middle::thir::ExprKind::ZstLiteral in the Rust's THIR representation that we do not handle. If we look at the span
field, we see that it appears in the source in the file serde/src/de/impls.rs
at line 778:
forwarded_impl! {
(T), Reverse<T>, Reverse // Here is the error
}
This is not very informative as this code is generated by a macro. Another similar kind of expression appears later:
impl<'de, T> Deserialize<'de> for Wrapping<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) > Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer).map(
// Here is the error:
Wrapping
)
}
}
The Wrapping
term is the constructor of a structure, used as a function. We add the support of this case in the pullrequest coqofrust#471.
When we typecheck the generated Coq code, we quickly get an error:
(* Generated by coqofrust *)
Require Import CoqOfRust.CoqOfRust.
Module lib.
Module core.
End core.
End lib.
Module macros.
End macros.
Module integer128.
End integer128.
Module de.
Module value.
Module Error.
Section Error.
Record t : Set := {
(* Here is the error: *)
err : ltac:(serde.de.value.ErrorImpl);
}.
(* 180.000 more lines! *)
The reason is that serde.de.value.ErrorImpl
is not yet defined here. In Coq, we must order the definitions in the order of dependencies to ensure that there are no nonterminating definitions with infinite recursive calls and to preserve the consistency of the system.
This issue does not seem easy to us, as in a Rust crate, everything can depend on each other:
impl
blocksOur current solutions are:
coqofrust
), but it is not very practical. Indeed, reordering elements in a big project generates a lot of conflicts in the version control system, especially if we cannot upstream the changes to the original project.In order to handle large projects, such as serde
, we need to find a more definitive solution to handle the order of dependencies.
Our idea is to use a more verbose, but simpler translation, to generate Coq code that is not sensitive to the ordering of Rust. In addition, we should have a more robust mechanism for the traits, as there are still some edge cases that we do not handle well.
Our main ingredients are:
Value
type. With this approach, we can represent mutually recursive Rust types, that are generally hard to translate in a sound manner to Coq. We should also avoid a lot of errors on the Coq side related to type inference.These ingredients have some drawbacks:
We rework our definitions of values, pointers and monad to represent the effects, taking into account the fact that we remove the types from the translation. Here are the main definitions that we are planning to use. We have not tested them yet as we need to update the translation to Coq to use them. We will do that just after.
Module Pointer.
Module Index.
Inductive t : Set :=
 Tuple (index : Z)
 Array (index : Z)
 StructRecord (constructor field : string)
 StructTuple (constructor : string) (index : Z).
End Index.
Module Path.
Definition t : Set := list Index.t.
End Path.
Inductive t (Value : Set) : Set :=
 Immediate (value : Value)
 Mutable {Address : Set} (address : Address) (path : Path.t).
Arguments Immediate {_}.
Arguments Mutable {_ _}.
End Pointer.
A pointer is either:
The type of Address
is not enforced yet, but we will do it when defining the semantics.
Module Value.
Inductive t : Set :=
 Bool : bool > t
 Integer : Integer.t > Z > t
(** For now we do not know how to represent floats so we use a string *)
 Float : string > t
 UnicodeChar : Z > t
 String : string > t
 Tuple : list t > t
 Array : list t > t
 StructRecord : string > list (string * t) > t
 StructTuple : string > list t > t
 Pointer : Pointer.t t > t
(** The two existential types of the closure must be [Value.t] and [M]. We
cannot enforce this constraint there yet, but we will do when defining the
semantics. *)
 Closure : {'(t, M) : Set * Set @ t > M} > t.
End Value.
Here, this type aims to represent any Rust value. We might add a few cases later to represent the dyn
values, for example. Most of the cases of this type are as expected:
StructRecord
is for constructors of struct
or enum
with named fields.StructTuple
is for constructors of struct
or enum
with unnamed fields.Pointer
is for pointers to data, that could be either &
, &mut
, *const
, or *mut
.Closure
is for closures (anonymous functions). To prevent errors with the positivity checker of Coq, we use an existential type for the type Value.t
(as well as M
, which will be defined later). Note that we are using impredicative Set
in Coq, and {A : Set @ P A}
is our notation for existential Set
in Set
. Without impredicative sets, we could have issues with the universe levels. The fact that these existential types are always Value.t
and M
will be enforced when defining the semantics.Module Primitive.
Inductive t : Set :=
 StateAlloc (value : Value.t)
 StateRead {Address : Set} (address : Address)
 StateWrite {Address : Set} (address : Address) (value : Value.t)
 EnvRead.
End Primitive.
Here are the IO calls to the system that the monad can make. This list might be extended later. For now, we mainly have primitives to access the memory.
Module LowM.
Inductive t (A : Set) : Set :=
 Pure : A > t A
 CallPrimitive : Primitive.t > (Value.t > t A) > t A
 Loop : t A > (A > bool) > (A > t A) > t A
 Impossible : t A
(** This constructor is not strictly necessary, but is used as a marker for
functions calls in the generated code, to help the tactics to recognize
points where we can compose about functions. *)
 Call : t A > (A > t A) > t A.
Arguments Pure {_}.
Arguments CallPrimitive {_}.
Arguments Loop {_}.
Arguments Impossible {_}.
Arguments Call {_}.
Fixpoint let_ {A : Set} (e1 : t A) (f : A > t A) : t A :=
match e1 with
 Pure v => f v
 CallPrimitive primitive k =>
CallPrimitive primitive (fun v => let_ (k v) f)
 Loop body is_break k =>
Loop body is_break (fun v => let_ (k v) f)
 Impossible => Impossible
 Call e k =>
Call e (fun v => let_ (k v) f)
end.
End LowM.
This is the first layer of our monad, very similar to what we had before. We remove the cast operation, as now everything has the same type. We use a style by continuation, but we also define a let_
function to have a "bind" operator. Note that we always have the same type as parameter, so this is not really a monad as the "bind" operator should have the type:
forall {A B : Set}, M A > (A > M B) > M B
Always having the same type is enough for us as we use a single type of all Rust values.
We have the same type as before for the exceptions, representing the panics and all the special control flow operations such as continue
, return
, and break
:
Module Exception.
Inductive t : Set :=
(** exceptions for Rust's `return` *)
 Return : Value.t > t
(** exceptions for Rust's `continue` *)
 Continue : t
(** exceptions for Rust's `break` *)
 Break : t
(** escape from a match branch once we know that it is not valid *)
 BreakMatch : t
 Panic : string > t.
End Exception.
Our final monad definition is a thin wrapper around LowM
, to add an error monad to propagate the exceptions:
Definition M : Set :=
LowM.t (Value.t + Exception.t).
Definition let_ (e1 : M) (e2 : Value.t > M) : M :=
LowM.let_ e1 (fun v1 =>
match v1 with
 inl v1 => e2 v1
 inr error => LowM.Pure (inr error)
end).
Once again, this is not really a monad as the type of the values that we compute is always the same, and we do not need more. Having a definition in two steps (LowM
and M
) is useful to separate the part that can be defined by computation (the M
part) from the part whose semantics can only be given by inductive predicates (the LowM
part).
Next, we will see how we can use this new definition of Rust values, whether it works to translate our examples, and most importantly, how to modify coqofrust
to generate terms without types.
If you are interested in formally verifying Rust projects, do not hesitate to get in touch with us at contact@formal.land or go to our GitHub repository for coqofrust
.
The goal is to formally verify Go programs to make them totally bugfree. It is actually possible to make a program totally bugfree, as formal verification can cover all execution cases and kinds of properties thanks to the use of mathematical methods. This corresponds to the highest level of the Evaluation Assurance Levels used for critical applications, such as the space industry.
All the code of our work is available on GitHub at github.com/formalland/coqofgoexperiment.
We believe that there are not yet a lot of formal verification tools for Go. We can cite Goose, which is working by translation from Go to the proof system Coq. We will follow a similar approach, translating the Go language to our favorite proof system Coq. In contrast to Goose, we plan to support the whole Go language, even at the expense of the simplicity of the translation.
For that, we target the translation of the SSA form of Go of Go instead of the Go AST. The SSA form is a more lowlevel representation of Go, so we hope to capture the semantics of the whole Go language more easily. This should be at the expense of the simplicity of the generated translation, but we hope that having full language support outweighs this.
Go is an interesting target as:
Among interesting properties that we can verify are:
panic
in the code,You can follow us on X to get our updates. We propose tools and services to make your codebase totally bugfree. Contact us at contact@formal.land to chat! We offer a free audit to assess the feasibility of formal verification on your case.
Our company goal is to make formal verification accessible to all projects, reducing its cost to 20% of the development cost. There should be no reason to have bugs in enduser products!
Our first target is to achieve the formal verification including all the dependencies of the hello world program:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
What we want to show about this code is that it does a single and only thing: outputting the string "Hello, World!" to the standard output. Its only dependency is the fmt
package, but when we look at the transitive dependencies of this package:
go list f '{{ .Deps }}' fmt
we get around forty packages:
errors
internal/abi
internal/bytealg
internal/coverage/rtcov
internal/cpu
internal/fmtsort
internal/goarch
internal/godebugs
internal/goexperiment
internal/goos
internal/itoa
internal/oserror
internal/poll
internal/race
internal/reflectlite
internal/safefilepath
internal/syscall/execenv
internal/syscall/unix
internal/testlog
internal/unsafeheader
io
io/fs
math
math/bits
os
path
reflect
runtime
runtime/internal/atomic
runtime/internal/math
runtime/internal/sys
runtime/internal/syscall
sort
strconv
sync
sync/atomic
syscall
time
unicode
unicode/utf8
unsafe
We will need to translate all these packages to meaningful Coq code.
We made the coqofgo
tool, with everything in a single file main.go for now. We retrieve the SSA form of a Go package provided as a command line parameter (code without the error handling):
func main() {
packageToTranslate := os.Args[1]
cfg := &packages.Config{Mode: packages.LoadSyntax}
initial, _ := packages.Load(cfg, packageToTranslate)
_, pkgs := ssautil.Packages(initial, 0)
pkgs[0].Build()
members := pkgs[0].Members
The SSA form of a program is generally used internally by compilers to have a simple representation to work on. The LLVM language is such an example. In SSA, each variable is assigned exactly once and the control flow is explicit, with jumps or conditional jumps to labels. There are no for
loops, if
statements, or nonprimitive expressions.
Then we iterate over all the SSA members
, and directly print the corresponding Coq code to the standard output. We do not use an intermediate representation or make intermediate passes. We do not even do prettyprinting (splitting lines that are too long at the right place, and introducing indentation)! This should not be necessary as the SSA code cannot nest subexpressions or statements. We still try to print a readable Coq code, as it will be used in the proofs.
There are four kinds of SSA members:
Named constants and globals are similar, and are for toplevel variables whose value is either known at compiletime or computed at the program's init. Types are for type definitions. We will focus on functions, as this is where the code is.
The SSA functions in Go are described by the type ssa.Function
:
type Function struct {
Signature *types.Signature
// source information
Synthetic string // provenance of synthetic function; "" for true source functions
Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error)
Prog *Program // enclosing program
Params []*Parameter // function parameters; for methods, includes receiver
FreeVars []*FreeVar // free variables whose values must be supplied by closure
Locals []*Alloc // frameallocated variables of this function
Blocks []*BasicBlock // basic blocks of the function; nil => external
Recover *BasicBlock // optional; control transfers here after recovered panic
AnonFuncs []*Function // anonymous functions directly beneath this one
// contains filtered or unexported fields
}
The main part of interest for us is Blocks
. A block is a sequence of instructions, and the control flow is explicit. The last instruction of a block is a jump to another block, or a return. The first instructions of a block can be the special Phi
instruction, which is used to merge control flow from different branches.
We decided to write a first version to see what the SSA code of Go looks like when printed in Coq, without thinking about generating a welltyped code. This looks like this:
with MakeUint64 (α : list Val.t) : M (list Val.t) :=
M.Thunk (
match α with
 [x] =>
M.Thunk (M.EvalBody [(0,
let* "t0" := Instr.BinOp x "<" (Val.Lit (Lit.Int 9223372036854775808)) in
Instr.If (Register.read "t0") 1 2
);
(1,
let* "t1" := Instr.Convert x in
let* "t2" := Instr.ChangeType (Register.read "t1") in
let* "t3" := Instr.MakeInterface (Register.read "t2") in
M.Return [(Register.read "t3")]
);
(2,
let* "t4" := Instr.Alloc (* complit *) Alloc.Local "*go/constant.intVal" in
let* "t5" := Instr.FieldAddr (Register.read "t4") 0 in
let* "t6" := Instr.Call (CallKind.Function (newInt [])) in
let* "t7" := Instr.Call (CallKind.Function (TODO_method [(Register.read "t6"); x])) in
do* Instr.Store (Register.read "t5") (Register.read "t7") in
let* "t8" := Instr.UnOp "*" (Register.read "t4") in
let* "t9" := Instr.MakeInterface (Register.read "t8") in
M.Return [(Register.read "t9")]
)])
 _ => M.Thunk (M.EvalBody [])
end)
for a source Go code (from the go/constant package):
// MakeUint64 returns the [Int] value for x.
func MakeUint64(x uint64) Value {
if x < 1<<63 {
return int64Val(int64(x))
}
return intVal{newInt().SetUint64(x)}
}
There are three blocks of code, labeled with 0
, 1
, and 2
. The first block ends with a conditional jump If
corresponding to the if
statement in the Go code. The following blocks are corresponding to the two possible branches of the if
statement. They both end with a Return
instruction, corresponding to the return
statement in the Go code. They run various primitive instructions that we have translated as we can.
The generated Coq code is still readable but more verbose than the original Go code. We will later develop proof techniques using simulations to enable the user to define equivalent but simpler versions of the translation. Being able to define simulations of an imperative program is also important for the proofs, as we can rewrite the code in functional style to make it easier to reason about.
From there, a second step is to have a generated code that typechecks, forgetting about making a code with sound semantics for now. We generate the various Coq definitions that are needed in a header of the generated code, using axioms for all the definitions. For example, for the allocations we do:
Module Alloc.
Inductive t : Set :=
 Heap
 Local.
End Alloc.
Module Instr.
Parameter Alloc : Alloc.t > string > M Val.t.
The Inductive
keyword in Coq defines a type with two constructors Heap
and Local
. The Parameter
keyword defines an axiomatized definition, where we only provide the type but not the definition itself. The Instr.Alloc
instruction takes as parameters an allocation mode Alloc.t
and a string and returns an M Val.t
value.
We make the choice to remove the types while doing the translation, as the type system of Go is probably incompatible with the one of Coq in many ways. We thus translate everything to a single type Val.t
in Coq to represent all kinds of possible Go values. The downside of this approach is that is makes the generated code less readable and less safe, as types are useful to track the correct use of values.
For now, we define the Val.t
type as:
Module Val.
Inductive t : Set :=
 Lit (_ : Lit.t)
 Tuple (_ : list t).
End Val.
with the literals Lit.t
as:
Module Lit.
Inductive t : Set :=
 Bool (_ : bool)
 Int (_ : Z)
 Float (_ : Rational)
 Complex (_ _ : Rational)
 String (_ : string)
 Nil.
End Lit.
We plan to refine this type and add more cases as we improve coqofgo
. Structures, pointers, and closures are missing for now.
In order to represent the sideeffects of the Go code, we use a monadic style. This is a standard approach to represent sideeffects like mutations, exceptions, or nontermination in a purely function language such as Coq. We choose to use:
M
of the monad. This simplifies the manipulation of the monad by allowing to compute on it and by delegating the actual implementation of the monadic primitives for later.In that sense, we follow the approach in the paper Modular, Compositional, and Executable Formal Semantics for LLVM IR, that is using a coinductive free monad (interaction tree) to formalize a reasonable subset of the LLVM language that is also an SSA representation but with more lowlevel instructions than Go.
Our definition for M
for now is:
Module M.
CoInductive t (A : Set) : Set :=
 Return (_ : A)
 Bind {B : Set} (_ : t B) (_ : B > t A)
 Thunk (_ : t A)
 EvalBody (_ : list (Z * t A)).
Arguments Return {A}.
Arguments Bind {A B}.
Arguments Thunk {A}.
Arguments EvalBody {A}.
End M.
Definition M : Set > Set := M.t.
We define all the functions that we translate as mutually recursive with the CoFixpoint ... with ...
keyword of Coq. Thus, we do not have to preserve the ordering of definitions that is required by Coq or care for recursive or mutually recursive functions in Go.
However, we did not achieve to make the typechecker of Coq happy for our CoFixpoint
as many definitions are axiomatized, and the typechecker of Coq wants their definitions to know if they produce coinductive constructors. So, for now, we admit this step by disabling the termination checker with this flag:
Local Unset Guard Checking.
When we translate our hello world example we get the Coq code:
CoFixpoint Main (α : list Val.t) : M (list Val.t) :=
M.Thunk (
match α with
 [] =>
M.Thunk (M.EvalBody [(0,
let* "t0" := Instr.Alloc (* varargs *) Alloc.Heap "*[1]any" in
let* "t1" := Instr.IndexAddr (Register.read "t0") (Val.Lit (Lit.Int 0)) in
let* "t2" := Instr.MakeInterface (Val.Lit (Lit.String "Hello, World!")) in
do* Instr.Store (Register.read "t1") (Register.read "t2") in
let* "t3" := Instr.Slice (Register.read "t0") None None in
let* "t4" := Instr.Call (CallKind.Function (fmt.Println [(Register.read "t3")])) in
M.Return []
)])
 _ => M.Thunk (M.EvalBody [])
end)
with init (α : list Val.t) : M (list Val.t) :=
M.Thunk (
match α with
 [] =>
M.Thunk (M.EvalBody [(0,
let* "t0" := Instr.UnOp "*" (Register.read "init$guard") in
Instr.If (Register.read "t0") 2 1
);
(1,
do* Instr.Store (Register.read "init$guard") (Val.Lit (Lit.Bool true)) in
let* "t1" := Instr.Call (CallKind.Function (fmt.init [])) in
Instr.Jump 2
);
(2,
M.Return []
)])
 _ => M.Thunk (M.EvalBody [])
end).
The init
function, which is automatically generated by the Go compiler to initialize global variables, does not do much here. It checks whether it was already called or not reading the init$guard
variable, and if not, it calls the fmt.init
function. The Main
function is the one that we are interested in. It allocates a variable to store the string "Hello, World!", and then calls the fmt.Println
function to print it.
From there, to continue the project we have two possibilities:
For the next step, we choose to follow the second possibility as we are more confident in being able to define the semantics of the instructions, which is purely done on the Coq side, than in being able to use the Go compiler's APIs to retrieve the definitions of all the dependencies and related them together.
We have presented the beginning of our journey to translate Go programs to Coq, to build a formal verification tool for Go. The translation typechecks on the few examples we have tried but has no semantics. We will follow by handling the translation of dependencies of a package.
If you are interested in this project, please contact us at contact@formal.land or go to our GitHub repository.
]]>Indeed, even with the use of a strict type system, there can still be bugs for properties that cannot be expressed with types. An example of such a property is the backward compatibility of an API endpoint for the new release of a web service when there has been code refactoring. Only formal verification can cover all execution cases and kinds of properties.
The code of the tool is at: github.com/formalland/coqofhsexperiment (AGPL license)
We propose tools to make your codebase totally bugfree. Contact us at contact@formal.land for more information! We offer a free audit to assess the feasibility of formal verification for your case.
We estimate that the cost of formal verification should be 20% of the development cost. There are no reasons to still have bugs today!
There are already some tools to formally verify Haskell programs:
In this experiment, we want to check the feasibility of translation from Haskell to Coq:
Here is an example of a Haskell function:
fixObvious :: (a > a) > a
fixObvious f = f (fixObvious f)
that coqofhs
translates to this valid Coq code:
CoFixpoint fixObvious : Val.t :=
(Val.Lam (fun (f : Val.t) => (Val.App f (Val.App fixObvious f)))).
We read the Haskell Core representation of Haskell using the GHC plugin system. Thus, we read the exact same code version as the one that is compiled down to assembly code by GHC, to take into account all compilation options.
Haskell Core is an intermediate representation of Haskell that is close to the lambda calculus and used by the Haskell compiler for various optimizations passes. Here are all the constructors of the Expr
type of Haskell Core:
data Expr b
= Var Id
 Lit Literal
 App (Expr b) (Arg b)
 Lam b (Expr b)
 Let (Bind b) (Expr b)
 Case (Expr b) b Type [Alt b]
 Cast (Expr b) Coercion
 Tick (Tickish Id) (Expr b)
 Type Type
 Coercion Coercion
This paper System FC, as implemented in GHC presents it as System F plus coercions. We translate Haskell code to an untyped version of the lambda calculus in Coq, with coinduction to allow for infinite data structures:
Module Val.
#[bypass_check(positivity)]
CoInductive t : Set :=
 Lit (_ : Lit.t)
 Con (_ : string) (_ : list t)
 App (_ _ : t)
 Lam (_ : t > t)
 Case (_ : t) (_ : t > list (Case.t t))
 Impossible.
End Val.
We make the translation by induction over the Haskell Core representation, and we translate each constructor to a corresponding constructor of the Coq representation. We prettyprint the Coq code directly without using an intermediate representation. We use the prettyprinter package with the two main following primitives:
concatNest :: [Doc ()] > Doc ()
concatNest = group . nest 2 . vsep
concatGroup :: [Doc ()] > Doc ()
concatGroup = group . vsep
to display a subterm with or without indentation when splitting lines that are too long. This translation works well on all the Haskell expressions that we have tested.
We have not yet defined a semantics. For now, the terms that we generate in Coq are purely descriptive. We will wait to have examples of things to verify to define semantics that are practical to use.
We have not yet translated typeclasses. The Haskell Core language hides most of the typeclassesrelated code. For example, it represents instances as additional function parameters for functions that have a typeclass constraints. But we still need to declare the functions corresponding to the member of the typeclasses, what we have not done yet.
We have not yet implemented the translation of multifile projects. We have only tested the translation of a singlefile project.
Similarly to the handling of multifile projects, we have not yet tested the translation of projects using external libraries or translating the base library of Haskell.
We had to turn off the strict positivity condition for the definition of Val.t
in Coq with:
#[bypass_check(positivity)]
This is for to the case:
 Lam (_ : t > t)
where t
appears as a parameter of a function (negative position). We do not know if this causes any problem in practice, on values that correspond to welltyped Haskell programs.
We have presented an experiment on the translation of Haskell programs to Coq. If you are interested in this project, please get in touch with us at contact@formal.land or go to the GitHub repository of the project.
]]>Ensuring Flawless Software in a Flawed World
In this blog post, we present what formal verification is and why this is such a valuable tool to improve the security of your applications.
If you want to formally verify your codebase to improve the security of your application, contact us at contact@formal.land! We offer a free audit of your codebase to assess the feasibility of formal verification.
The current development of our tool coqofrust, for the formal verification of Rust code, is made possible thanks to the Aleph Zero's Foundation and its Ecosystem Funding Program. The aim is to develop an extra safe platform to build decentralized applications with formally verified smart contracts.
Formal verification is a set of techniques to check for the complete correctness of a program, reasoning at a symbolic level rather than executing a particular instance of the code. By symbolic reasoning, we mean following the values of the variables by tracking their names and constraints, without necessarily giving them an example value. This is what we would do in our heads to understand a code where a variable username
appears, following which functions it is given to, to know where we use the user name. The concrete user name that we consider is irrelevant, although some people prefer to think with an example.
In formal verification, we rely on precise mathematical reasoning to make sure that there are no mistakes or missing cases. We check this reasoning with a dedicated program (SMT solver, Coq proof system, ...). Indeed, as programs grow in complexity, it could be easy to forget an if
branch or an error case.
For example, to say that the following Rust program is valid:
/// Return the maximum of [a] and [b]
fn get_max(a: u128, b: u128) > u128 {
if a > b {
a
} else {
b
}
}
we reason on two cases (reasoning by disjunction):
a > b
where a
is the maximum,a <= b
where b
is the maximum,with the values of a
and b
being irrelevant (symbolic). In both cases, we can conclude that get_max
returns the maximum.
This is in contrast with testing, where we need to execute the program with all possible instances of a
and b
to check that the program is correct with 100% certainty. This is infeasible in this case as the type u128
is too large to be tested exhaustively: there are 2^256
possible values for a
and b
, meaning 115792089237316195423570985008687907853269984665640564039457584007913129639936
possible values!
A program is shown correct with respect to an expected behavior, called a formal specification. This is expressed in a mathematical language to be nonambiguous. For example, we can specify the behavior of the previous program as:
FORALL (a b : u128),
(get_max a b = a OR get_max a b = b) AND
(get_max a b >= a AND get_max a b >= b)
stating that we indeed return the maximum of a
and b
.
When a program is formally verified, we are mathematically sure it will always follow its specifications. This is a way to eliminate all bugs, as long as we have a complete specification of what it is supposed to do or not do. This corresponds to the highest level of Evaluation Assurance Level, EAL7. This is used for critical applications, such as space rocket software, where a single bug can be extremely expensive (the loss of a rocket!).
There are various formal verification tools, such as the proof system Coq. The C compiler CompCert is an example of large software verified in Coq. It is proven correct, in contrast to most other C compilers that contain subtle bugs. CompCert is now used by Airbus to compile C programs embedded in planes 🛫.
Formal verification is extremely useful as it can anticipate all the bugs by exploring all possible execution cases of a program. Here is a quote from Edsger W. Dijkstra:
Program testing can be used to show the presence of bugs, but never to show their absence!
It offers the possibility to make software that never fails. This is often required for applications with human life at stake, such as planes or medical devices. But it can also be useful for applications where a single bug can be extremely expensive, such as financial applications.
Smart contracts are a good example of such applications. They are programs that are executed on a blockchain and are used to manage assets worth billions of dollars. A single bug in a smart contract can lead to the loss of all the assets managed by the contract. In the first half of 2023, some estimate that attacks on web3 platforms resulted in a loss of $655.61 million, with most of these losses due to bugs in smart contracts. These bugs could be prevented using formally verified smart contracts.
Finally, formal verification is useful to improve the quality of a program by enforcing the need to use:
Compared to testing, formal verification is more complex as:
In addition, formal verification assumes a certain model of the environment of the program, which is not always accurate. When actually executing the code, we also exercise all the dependencies (libraries, operating system, network, ...) that might cause issues at runtime.
However, formal verification is the only way to have an exhaustive check of the program. It verifies all corner cases, such as integer overflows, or hardtoreproduce issues, such as concurrency bugs. We recommend combining both approaches as they do not catch the same kinds of bugs.
At Formal Land, we consider it critical to lower the cost of formal verification to apply it to a larger scope of programs and prevent more bugs and attacks. We work on the formal verification of Rust with coqofrust and OCaml with coqofocaml.
Formal verification is a powerful tool to improve the security of your applications. It is the only way to prevent all bugs by exploring all possible executions of your programs. It complements existing testing methods. It is particularly useful for critical applications, such as smart contracts, where a single bug can be extremely expensive.
]]>Overall, we are now able to translate about 80% of the Rust examples from the Rust by Example book into valid Coq files. This means we support a large subset of the Rust language.
To formally verify your Rust codebase and improve the security of your application, email us at contact@formal.land! Formal verification is the only way to prevent all bugs by exploring all possible executions of your programs 🎯.
This work and the development of coqofrust is made possible thanks to the Aleph Zero's Foundation, to develop an extra safe platform to build decentralized applications with formally verified smart contracts.
The tool coqofrust
is tied to a particular version of the Rust compiler that we use to parse and typecheck a cargo
project. We now support the nightly20231215
version of Rust, up from nightly20230430
. Most of the changes were minor, but it is good to handle these regularly to have smooth upgrades. The corresponding pull request is coqofrust/pull/445. We also got more Clippy warnings thanks to the new version of Rust.
The traits of Rust are similar to the typeclasses of Coq. This is how we translate traits to Coq.
But there are a lot of subtle differences between the two languages. The typeclass inference mechanism of Coq does not work all the time on generated Rust code, even when adding a lot of code annotations. We think that the only reliable way to translate Rust traits would be to explicit the implementations inferred by the Rust compiler, but the Rust compiler currently throws away this information.
Instead, our new solution is to use a Coq tactic:
(** Try first to infer the trait instance, and if unsuccessful, delegate it at
proof time. *)
Ltac get_method method :=
exact (M.pure (method _)) 
exact (M.get_method method).
that first tries to infer the trait instance for a particular method, and if it fails, delegates its definition to the user at proof time. This is a bit unsafe, as a user could provide invalid instances at proof time, by giving some custom instance definitions instead of the ones generated by coqofrust
. So, one should be careful to only apply generated instances to fill the hole made by this tactic in case of failure. We believe this to be a reasonable assumption that we could enforce someday if needed.
We are also starting to remove the trait constraints on polymorphic functions (the where
clauses). We start by doing it in our manual definition of the standard library of Rust. The rationale is that we can provide the actual trait instances at proof time by having the right hypothesis replicating the constraints of the where
clauses. Having fewer where
clauses reduces the complexity of the type inference of Coq on the generated code. There are still some cases that we need to clarify, for example, the handling of associated types in the absence of traits.
We have a definition of the standard library of Rust, mainly composed of axiomatized^{1} definitions, in these three folders:
By adding more of these axioms, as well as with some small changes to the coqofrust
tool, we are now able to successfully translate around 80% of the examples of the Rust by Example book. There can still be some challenges on larger programs, but this showcases the good support of coqofrust
for the Rust language.
We are continuing to improve our tool coqofrust
to support more of the Rust language and are making good progress. If you need to improve the security of critical applications written in Rust, contact us at contact@formal.land to start formally verifying your code!
An axiom in Coq is either a theorem whose proof is admitted, or a function/constant definition left for latter. This is the equivalent in Rust of the todo!
macro. ↩
Our tool coqofrust
works by translating Rust programs to the general proof system 🐓 Coq. Here we explain how we translate match
patterns from Rust to Coq. The specificity of Rust patterns is to be able to match values either by value or reference.
To formally verify your Rust codebase and improve the security of your application, email us at contact@formal.land! Formal verification is the only way to prevent all bugs by exploring all possible executions of your program.
This work and the development of coqofrust is made possible thanks to the Aleph Zero's Foundation, to develop an extra safe platform to build decentralized applications with formally verified smart contracts.
To illustrate the pattern matching in Rust, we will use the following example featuring a match by reference:
pub(crate) fn is_option_equal<A>(
is_equal: fn(x: &A, y: &A) > bool,
lhs: Option<A>,
rhs: &A,
) > bool {
match lhs {
None => false,
Some(ref value) => is_equal(value, rhs),
}
}
We take a function is_equal
as a parameter, operating only on references to the type A
. We apply it to compare two values lhs
and rhs
:
lhs
is None
, we return false
,lhs
is Some
, we get its value by reference and apply is_equal
.When we apply the pattern:
Some(ref value) => ...
we do something interesting: we read the value of lhs
to know if we are in a Some
case but leave it in place and return value
the reference to its content.
To simulate this behavior in Coq, we need to match in two steps:
lhs
to know if we are in a Some
case or not,Some
case, create the reference to the content of a Some
case based on the reference to lhs
.The Coq translation that our tool coqofrust generates is the following:
Definition is_option_equal
{A : Set}
(is_equal : (ref A) > (ref A) > M bool.t)
(lhs : core.option.Option.t A)
(rhs : ref A)
: M bool.t :=
let* is_equal := M.alloc is_equal in
let* lhs := M.alloc lhs in
let* rhs := M.alloc rhs in
let* α0 : M.Val bool.t :=
match_operator
lhs
[
fun γ =>
(let* α0 := M.read γ in
match α0 with
 core.option.Option.None => M.alloc false
 _ => M.break_match
end) :
M (M.Val bool.t);
fun γ =>
(let* α0 := M.read γ in
match α0 with
 core.option.Option.Some _ =>
let γ0_0 := γ.["Some.0"] in
let* value := M.alloc (borrow γ0_0) in
let* α0 : (ref A) > (ref A) > M bool.t := M.read is_equal in
let* α1 : ref A := M.read value in
let* α2 : ref A := M.read rhs in
let* α3 : bool.t := M.call (α0 α1 α2) in
M.alloc α3
 _ => M.break_match
end) :
M (M.Val bool.t)
] in
M.read α0.
We run the match_operator
on lhs
and the two branches of the match
. This operator is of type:
Definition match_operator {A B : Set}
(scrutinee : A)
(arms : list (A > M B)) :
M B :=
...
It takes a scrutinee
value to match as a parameter, and runs a sequence of functions arms
on it. Each function arms
takes the value of the scrutinee
and returns a monadic value M B
. This monadic value can either be a success value if the pattern matches, or a special failure value if the pattern does not match. We evaluate the branches until one succeeds.
None
branchThe None
branch is the simplest one. We read the value at the address given by lhs
(we represent each Rust variable by its address) and match it with the None
constructor:
fun γ =>
(let* α0 := M.read γ in
match α0 with
 core.option.Option.None => M.alloc false
 _ => M.break_match
end) :
M (M.Val bool.t)
If it matches, we return false
. If it does not, we return the special value M.break_match
to indicate that the pattern does not match.
Some
branchIn the Some
branch, we first also read the value at the address given by lhs
and match it with the Some
constructor:
fun γ =>
(let* α0 := M.read γ in
match α0 with
 core.option.Option.Some _ =>
let γ0_0 := γ.["Some.0"] in
let* value := M.alloc (borrow γ0_0) in
let* α0 : (ref A) > (ref A) > M bool.t := M.read is_equal in
let* α1 : ref A := M.read value in
let* α2 : ref A := M.read rhs in
let* α3 : bool.t := M.call (α0 α1 α2) in
M.alloc α3
 _ => M.break_match
end) :
M (M.Val bool.t)
If we are in that case, we create the value:
let γ0_0 := γ.["Some.0"] in
with the address of the first field of the Some
constructor, relative to the address of lhs
given in γ
. We define the operator .["Some.0"]
when we define the option type and generate such definitions for all userdefined enum types.
We then encapsulate the address γ0_0
in a proper Rust reference:
let* value := M.alloc (borrow γ0_0) in
of type ref A
in the original Rust code. Finally, we call the function is_equal
on the two references value
and rhs
, with some boilerplate code to read and allocate the variables.
We generalize this translation to all patterns by:

so that only patterns with a single choice remain,match_operator
operator,M.break_match
and continue with the next branch.At least one branch should succeed as the Rust compiler checks that all cases are covered. We still have a special value M.impossible
in Coq for the case where no patterns match and satisfy the type checker.
We distinguish and handle the following kind of patterns (and all their combinations):
_
,(ref) name
or (ref) name as pattern
(the ref
keyword is optional),Name { field1: pattern1, ... }
or Name(pattern1, ...)
(pattern1, ...)
,12
, true
, ...,[first, second, tail @ ..]
,&pattern
.This was enough to cover all of our examples. The Rust compiler can also automatically add some ref
patterns when matching on references. We do not need to handle this case as this is automatically done by the Rust compiler during its compilation to the intermediate THIR representation, and e directly read the THIR code.
In this blog post, we have presented how we translate Rust patterns to the proof system Coq. The difficult part is handling the ref
patterns, which we do by matching in two steps: matching on the values and then computing the addresses of the subfields.
If you have Rust smart contracts or programs to verify, feel free to email us at contact@formal.land. We will be happy to help!
]]>Here, we show how we formally verify an ERC20 smart contract written in Rust for the Aleph Zero blockchain. ERC20 smart contracts are used to create new kinds of tokens in an existing blockchain. Examples are stablecoins such as the 💲USDT.
To formally verify your Rust codebase and improve the security of your application, email us at contact@formal.land! Formal verification is the only way to prevent all bugs by exploring all possible executions of your program.
This work and the development of coqofrust is made possible thanks to the Aleph Zero's Foundation, to develop an extra safe platform to build decentralized applications with formally verified smart contracts.
Here is the Rust code of the smart contract that we want to verify:
#[ink::contract]
mod erc20 {
use ink::storage::Mapping;
#[ink(storage)]
#[derive(Default)]
pub struct Erc20 {
total_supply: Balance,
balances: Mapping<AccountId, Balance>,
allowances: Mapping<(AccountId, AccountId), Balance>,
}
#[ink(event)]
pub struct Transfer {
// ...
}
#[ink(event)]
pub struct Approval {
// ...
}
#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum Error {
// ...
}
pub type Result<T> = core::result::Result<T, Error>;
impl Erc20 {
#[ink(constructor)]
pub fn new(total_supply: Balance) > Self {
let mut balances = Mapping::default();
let caller = Self::env().caller();
balances.insert(caller, &total_supply);
Self::env().emit_event(Transfer {
from: None,
to: Some(caller),
value: total_supply,
});
Self {
total_supply,
balances,
allowances: Default::default(),
}
}
#[ink(message)]
pub fn total_supply(&self) > Balance {
self.total_supply
}
#[ink(message)]
pub fn balance_of(&self, owner: AccountId) > Balance {
self.balance_of_impl(&owner)
}
#[inline]
fn balance_of_impl(&self, owner: &AccountId) > Balance {
self.balances.get(owner).unwrap_or_default()
}
#[ink(message)]
pub fn allowance(&self, owner: AccountId, spender: AccountId) > Balance {
self.allowance_impl(&owner, &spender)
}
#[inline]
fn allowance_impl(&self, owner: &AccountId, spender: &AccountId) > Balance {
self.allowances.get((owner, spender)).unwrap_or_default()
}
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) > Result<()> {
let from = self.env().caller();
self.transfer_from_to(&from, &to, value)
}
#[ink(message)]
pub fn approve(&mut self, spender: AccountId, value: Balance) > Result<()> {
let owner = self.env().caller();
self.allowances.insert((&owner, &spender), &value);
self.env().emit_event(Approval {
owner,
spender,
value,
});
Ok(())
}
#[ink(message)]
pub fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) > Result<()> {
let caller = self.env().caller();
let allowance = self.allowance_impl(&from, &caller);
if allowance < value {
return Err(Error::InsufficientAllowance)
}
self.transfer_from_to(&from, &to, value)?;
// We checked that allowance >= value
#[allow(clippy::arithmetic_side_effects)]
self.allowances
.insert((&from, &caller), &(allowance  value));
Ok(())
}
fn transfer_from_to(
&mut self,
from: &AccountId,
to: &AccountId,
value: Balance,
) > Result<()> {
let from_balance = self.balance_of_impl(from);
if from_balance < value {
return Err(Error::InsufficientBalance)
}
// We checked that from_balance >= value
#[allow(clippy::arithmetic_side_effects)]
self.balances.insert(from, &(from_balance  value));
let to_balance = self.balance_of_impl(to);
self.balances
.insert(to, &(to_balance.checked_add(value).unwrap()));
self.env().emit_event(Transfer {
from: Some(*from),
to: Some(*to),
value,
});
Ok(())
}
}
}
This whole code is rather short and contains no loops, which will simplify our verification process. It uses a lot of macros, such as #[ink(message)]
, that are specific to the ink! language for smart contracts, built on top of Rust. To verify this smart contract, we removed all the macros and added a mock of the dependencies, such as ink::storage::Mapping
to get a map data structure.
By running our tool coqofrust we automatically obtain the corresponding Coq code for the contract erc20.v. Here is an extract for the transfer
function:
(*
fn transfer(&mut self, to: AccountId, value: Balance) > Result<()> {
let from = self.env().caller();
self.transfer_from_to(&from, &to, value)
}
*)
Definition transfer
(self : mut_ref ltac:(Self))
(to : erc20.AccountId.t)
(value : ltac:(erc20.Balance))
: M ltac:(erc20.Result unit) :=
let* self : M.Val (mut_ref ltac:(Self)) := M.alloc self in
let* to : M.Val erc20.AccountId.t := M.alloc to in
let* value : M.Val ltac:(erc20.Balance) := M.alloc value in
let* from : M.Val erc20.AccountId.t :=
let* α0 : mut_ref erc20.Erc20.t := M.read self in
let* α1 : erc20.Env.t :=
M.call (erc20.Erc20.t::["env"] (borrow (deref α0))) in
let* α2 : M.Val erc20.Env.t := M.alloc α1 in
let* α3 : erc20.AccountId.t :=
M.call (erc20.Env.t::["caller"] (borrow α2)) in
M.alloc α3 in
let* α0 : mut_ref erc20.Erc20.t := M.read self in
let* α1 : u128.t := M.read value in
let* α2 : core.result.Result.t unit erc20.Error.t :=
M.call
(erc20.Erc20.t::["transfer_from_to"] α0 (borrow from) (borrow to) α1) in
let* α0 : M.Val (core.result.Result.t unit erc20.Error.t) := M.alloc α2 in
M.read α0.
More details of the translation are given in previous blog posts, but basically:
let*
.We verify the code in two steps:
That way, we can eliminate all the memoryrelated operations by showing the equivalence with a simulation. Then, we can focus on the functional code, which is more straightforward to reason about. We can cite another project, Aeneas, which proposes to do the first step (removing memory operations) automatically.
We will work on the example of the transfer
function. We define the simulations in Simulations/erc20.v. For the transfer
function this is:
Definition transfer
(env : erc20.Env.t)
(to : erc20.AccountId.t)
(value : ltac:(erc20.Balance)) :
MS? State.t ltac:(erc20.Result unit) :=
transfer_from_to (Env.caller env) to value.
The function transfer
is a wrapper around transfer_from_to
, using the smart contract caller as the from
account. The monad MS?
combines the state and error effect. The state is given by the State.t
type:
Module State.
Definition t : Set := erc20.Erc20.t * list erc20.Event.t.
End State.
It combines the state of the contract (type Self
in the Rust code) and a list of events to represent the logs. The errors of the monad include panic errors, as well as control flow primitives such as return
or break
that we implement with exceptions.
We write all our proofs in Proofs/erc20.v. The lemma stating that the simulation is equivalent to the original code is:
Lemma run_transfer
(env : erc20.Env.t)
(storage : erc20.Erc20.t)
(to : erc20.AccountId.t)
(value : ltac:(erc20.Balance))
(H_storage : Erc20.Valid.t storage)
(H_value : Integer.Valid.t value) :
let state := State.of_storage storage in
let self := Ref.mut_ref Address.storage in
let simulation :=
lift_simulation
(Simulations.erc20.transfer env to value) storage in
{{ Environment.of_env env, state 
erc20.Impl_erc20_Erc20_t_2.transfer self to value ⇓
simulation.(Output.result)
 simulation.(Output.state) }}.
The main predicate is:
{{ env, state  translated_code ⇓ result  final_state }}.
This predicate defines our semantics, explaining how to evaluate a translated Rust code in an environment env
and a state state
, to obtain a result result
and a final state final_state
. We use an environment in addition to a state to initialize various globals and other information related to the execution context. For example, here, we use the environment to store the caller
of the contract and the pointer to the list of logs.
We define our monad for the translated code M A
in a style by continuation:
Inductive t (A : Set) : Set :=
 Pure : A > t A
 CallPrimitive {B : Set} : Primitive.t B > (B > t A) > t A
 Cast {B1 B2 : Set} : B1 > (B2 > t A) > t A
 Impossible : t A.
Arguments Pure {_}.
Arguments CallPrimitive {_ _}.
Arguments Cast {_ _ _}.
Arguments Impossible {_}.
For now, we use the primitives to access the memory and the environment:
Module Primitive.
Inductive t : Set > Set :=
 StateAlloc {A : Set} : A > t (Ref.t A)
 StateRead {Address A : Set} : Address > t A
 StateWrite {Address A : Set} : Address > A > t unit
 EnvRead {A : Set} : t A.
End Primitive.
For each of our monad constructs, we add a case to our evaluation predicate that we will describe:
Pure
The result is the value itself, and the state is unchanged:
 Pure :
{{ env, state'  LowM.Pure result ⇓ result  state' }}
Cast
The evaluation is only possible when B1
and B2
are the same type B
:
 Cast {B : Set} (state : State) (v : B) (k : B > LowM A) :
{{ env, state  k v ⇓ result  state' }} >
{{ env, state  LowM.Cast v k ⇓ result  state' }}
k
of the cast. We do not change the state in the cast.State.read
, checking that the address
is indeed allocated (it returns None
otherwise). Note that the type of v
depends on its address. We directly allocate values with their original type, to avoid serializations/deserializations to represent the state.
 CallPrimitiveStateRead
(address : Address) (v : State.get_Set address)
(state : State)
(k : State.get_Set address > LowM A) :
State.read address state = Some v >
{{ env, state  k v ⇓ result  state' }} >
{{ env, state 
LowM.CallPrimitive (Primitive.StateRead address) k ⇓ result
 state' }}
State.alloc_write
, that only succeeds for allocated addresses:
 CallPrimitiveStateWrite
(address : Address) (v : State.get_Set address)
(state state_inter : State)
(k : unit > LowM A) :
State.alloc_write address state v = Some state_inter >
{{ env, state_inter  k tt ⇓ result  state' }} >
{{ env, state 
LowM.CallPrimitive (Primitive.StateWrite address v) k ⇓ result
 state' }}
 CallPrimitiveStateAllocNone {B : Set}
(state : State) (v : B)
(k : Ref B > LowM A) :
{{ env, state  k (Ref.Imm v) ⇓ result  state' }} >
{{ env, state 
LowM.CallPrimitive (Primitive.StateAlloc v) k ⇓ result
 state' }}
 CallPrimitiveStateAllocSome
(address : Address) (v : State.get_Set address)
(state : State)
(k : Ref (State.get_Set address) > LowM A) :
let r :=
Ref.MutRef (A := State.get_Set address) (B := State.get_Set address)
address (fun full_v => full_v) (fun v _full_v => v) in
State.read address state = None >
State.alloc_write address state v = Some state' >
{{ env, state  k r ⇓ result  state' }} >
{{ env, state 
LowM.CallPrimitive (Primitive.StateAlloc v) k ⇓ result
 state' }}
State.read
should return None
. At this point, we can make any choice of unallocated address in order to simplify the proofs later. CallPrimitiveEnvRead
(state : State) (k : Env > LowM A) :
{{ env, state  k env ⇓ result  state' }} >
{{ env, state 
LowM.CallPrimitive Primitive.EnvRead k ⇓ result
 state' }}
We can make a few remarks about our semantics:
M.Impossible
as this primitive corresponds to impossible branches in the code.run
to evaluate a monadic program in a certain environment and state. Indeed, the user needs to make a choice during the allocation of new values, to know if we allocate the value as immutable or mutable, and with which address. The M.Cast
operator is also not computable, as we cannot decide if two types are equal.State
, as well as the primitives State.read
and State.alloc_write
, as long as they verify wellformedness properties. For example, reading after a write at the same address should return the written value. One should choose a State
that simplifies its proofs the most. To verify the smart contract, we have taken a record with two fields:
Self
type in Rust),M.Call
corresponding to a bind, to explicit the points in the code where we call userdefined functions. This is not necessary but helpful to track things in the proofs. Otherwise, the monadic bind is defined as a fixpoint with:
Fixpoint bind {A B : Set} (e1 : t A) (f : A > t B) : t B :=
match e1 with
 Pure v => f v
 CallPrimitive primitive k =>
CallPrimitive primitive (fun v => bind (k v) f)
 Cast v k =>
Cast v (fun v' => bind (k v') f)
 Impossible => Impossible
end.
return
/break
exceptions, we wrap our monad into an error monad:
Definition M (A : Set) : Set :=
LowM (A + Exception.t).
LowM
is the monad without errors as defined above and Exception.t
is:
Module Exception.
Inductive t : Set :=
(** exceptions for Rust's `return` *)
 Return {A : Set} : A > t
(** exceptions for Rust's `continue` *)
 Continue