Skip to main content

🦀 Example of verification for the Move's checker of Sui

· 8 min read

We are continuing our formal verification work for the implementation of the type-checker of the Move language in the 💧 Sui blockchain. We verify a manual translation in the proof system 🐓 Coq of the 🦀 Rust code of the Move checker as available on GitHub.

In this blog post, we present in detail the verification of a particular function AbstractStack::pop_eq_n that manipulates 📚 stacks of types to show that it is equivalent to its naive implementation.

All the code presented here is on our GitHub at github.com/formal-land/coq-of-rust 🧑‍🏫.

Get started

To ensure your code is secure today, contact us at  💌contact@formal.land! 🚀

Formal verification goes further than traditional audits to make 100% sure you cannot lose your funds, thanks to mathematical reasoning on the code. It can be integrated into your CI pipeline to check that every commit is fully correct without doing a whole audit again.

We make bugs such as the DAO hack ($60 million stolen) virtually impossible to happen again.

Water in forest

🕵️ The code to verify

Here is the definition in Rust of an AbstractStack, from the file move-abstract-stack/src/lib.rs:

/// An abstract value that compresses runs of the same value to reduce space usage
pub struct AbstractStack<T> {
values: Vec<(u64, T)>,
len: u64,
}

It says that a stack of elements of type T is a vector of pairs of a number and a value. The number is the number of times the value is repeated in the stack. The field len is the total number of elements in the stack. This representation is more efficient than a naive stack, in case the stack contains many repeated values.

Here is one of the primitives to remove elements from this stack:

/// Pops n values off the stack, erroring if there are not enough items or if the n items are
/// not equal
pub fn pop_eq_n(&mut self, n: NonZeroU64) -> Result<T, AbsStackError> {
let n: u64 = n.get();
if self.is_empty() || n > self.len {
return Err(AbsStackError::Underflow);
}
let (count, last) = self.values.last_mut().unwrap();
debug_assert!(*count > 0);
let ret = match (*count).cmp(&n) {
Ordering::Less => return Err(AbsStackError::ElementNotEqual),
Ordering::Equal => {
let (_, last) = self.values.pop().unwrap();
last
}
Ordering::Greater => {
*count -= n;
last.clone()
}
};
self.len -= n;
Ok(ret)
}

This function removes n elements from the stack, returning the value of removed elements. It returns an error if there are not enough elements in the stack or if the n last items are not grouped as equal elements.

Our goal is to show that this function is equal to the naive pop function with repetition on flattened stacks.

⚖️ Specification

Here is the property we want to verify in the formal language Coq:

Lemma flatten_pop_eq_n {A : Set} `{Eq.Trait A} (n : Z) (stack : AbstractStack.t A)
(H_n : n > 0) :
match AbstractStack.pop_eq_n n stack with
| Panic.Value (Result.Ok item, stack') =>
flatten stack = List.repeat item (Z.to_nat n) ++ flatten stack'
| _ => True
end.

It says that for any possible stack and n greater than 0, if we remove n elements from the stack and when the execution succeeds, the flattened stack is equal to the repetition of the removed element n times followed by the flattened stack.

How did we get from the Rust code above to the expression of this property? We manually converted the Rust code above in Coq with the following definitions:

Module AbstractStack.
Record t (A : Set) : Set := {
values : list (Z * A);
len : Z;
}.

for the AbstractStack type, and:

Definition pop_eq_n {A : Set} (n : Z) : MS! (t A) (Result.t A AbsStackError.t) :=
fun (self : t A) =>
if (is_empty self || (n >? len self))%bool then
return! (Result.Err AbsStackError.Underflow, self)
else
let! (count, last) := Option.unwrap (List.hd_error self.(values)) in
if count <? n then
return! (Result.Err AbsStackError.ElementNotEqual, self)
else if count =? n then
let (_, values) := vec.pop_front self.(values) in
let self := {|
values := values;
len := self.(len) - n
|} in
return! (Result.Ok last, self)
else
let! values :=
match values self with
| [] => panic! "unreachable"
| (_, last) :: values => return! ((count - n, last) :: values)
end in
let self := {|
values := values;
len := self.(len) - n
|} in
return! (Result.Ok last, self).

for the pop_eq_n function. Note that this definition uses a lot of user-defined notations, such as let!, that we made in order to simplify the expression of effects in Coq. You can read more about these notations on our previous blog post 🦀 Formal verification of the type checker of Sui – part 2. We checked by testing that our translation above behaves as the original Rust code, as explained in our blog post 🦀 Formal verification of the type checker of Sui – part 3. It is not necessary to understand the translation in detail, as its verification will flow naturally.

We define the flatten function to translate a stack with repetitions to a flat stack as:

Definition flatten {A : Set} (abstract_stack : AbstractStack.t A) : list A :=
List.flat_map (fun '(n, v) => List.repeat v (Z.to_nat n)) abstract_stack.(AbstractStack.values).

It duplicates all the elements n times with List.repeat v (Z.to_nat n) and concatenates them with List.flat_map.

🤓 Proof

To show that the specification above is correct for any stacks, we cannot test it as it will only cover a finite amount of cases. We must write a Coq proof showing by mathematical reasoning that the code is always correct.

Here is our full proof:

Proof.
destruct stack as [stack].
unfold AbstractStack.pop_eq_n, flatten.
(* if (is_empty self || (n >? len self))%bool then *)
destruct (_ || _); simpl; [reflexivity|].
unfold List.hd_error.
(* Option.unwrap (List.hd_error self.(values)) *)
destruct stack as [|[count last] stack]; simpl; [reflexivity|].
(* if count <? n then *)
destruct (_ <? n)%Z eqn:?; simpl; [reflexivity|].
(* if count =? n then *)
destruct (_ =? n)%Z eqn:?; simpl.
{ now replace n with count by lia. }
{ rewrite List.app_assoc.
rewrite <- List.repeat_app.
now replace (Z.to_nat n + Z.to_nat (count - n))%nat
with (Z.to_nat count)
by lia.
}
Qed.

The way it works is that we follow all the possible execution branches in the Coq definition of pop_eq_n to show that each branch leads to either an execution error or a Result.Ok value with the correct stack. The destruct tactic is the one that allows us to explore each branch of the code, and is used every time we have a match or an if in the code.

Other reasoning operations include unfold to expand a definition or rewrite and replace to replace an expression with another when we have proved they are equal. For example, we replace:

Z.to_nat n + Z.to_nat (count - n)

by:

Z.to_nat count

as addition and subtraction by n cancel each other, and count is greater than n in this context for the Z.to_nat (count - n) operation to be well-defined.

While we write the proof in Coq we get a better view thanks to the interactive proof mode. For example, after the last destruct we have:

A: Set
H: Eq.Trait A
n, count: Z
last: A
stack: list (Z * A)
len: Z
H_n: n >= 0
Heqb: (count <? n) = false
Heqb0: (count =? n) = true

--------------------------------------

1/2
List.repeat last (Z.to_nat count) ++ List.flat_map (fun '(n0, v) => List.repeat v (Z.to_nat n0)) stack =
List.repeat last (Z.to_nat n) ++ List.flat_map (fun '(n0, v) => List.repeat v (Z.to_nat n0)) stack

--------------------------------------

2/2
List.repeat last (Z.to_nat count) ++ List.flat_map (fun '(n0, v) => List.repeat v (Z.to_nat n0)) stack =
List.repeat last (Z.to_nat n) ++
List.repeat last (Z.to_nat (count - n)) ++ List.flat_map (fun '(n0, v) => List.repeat v (Z.to_nat n0)) stack

This is how we can progress in the proof and know which command to type. We see two sub-goals (1/2) and (2/2) for each branch explored by the last destruct. In both cases, we need to show an equality:

  1. The first one is solved by the fact that count = n in this branch.
  2. The second one is solved by the fact that count > n in this branch, so that we can group the List.repeat last (Z.to_nat n) with List.repeat last (Z.to_nat (count - n)) (repeating a "negative" number of times is the empty list so we need to make sure that count - n is not negative).

✒️ Conclusion

In this example, we have seen how to verify that the pop_eq_n function of the AbstractStack type in the Move checker of Sui is equivalent to the naive pop function with repetition on flattened stacks. As this is a formal proof, we are sure that this property holds for any possible stack and value of n.

We are continuing the work to verify the other functions of the project, with the final aim to verify the whole type-checker. We will keep you updated on our progress in the next blog posts 🚀.

For more

Follow us on X or LinkedIn for more, or comment on this post below! Feel free to DM us for any questions or requests!