In consensus protocols such as Paxos and Raft, we typically need a datatype representing a subset of some finite set (the basis set) and we need to test whether subset contains a majority of the basis elements.

The key fact we need to know about majorities is that two majorities always have an alement in common. We will call this the majority intersection property. Here, we will develop an abstract datatype for subsets of a finite basis set provided with the operations for building subsets and a computable majority prediate satisfying the the majority intersection property.

Our datatype takes a type basis as a parameter. This must be a sequence type equipped with a value max representing the maximum value of the sequence. Sequence types provide a total order, a zero element, schemata for induction and recursion, and an arithmetic theory.

The module provides the following interface:

1) a type this representing subsets of the basis type in the range [0,max-1] 2) a member relation 3) a predicate majority on sets that is true if the cardinality is > n/2. 4) an action empty returning the empty set 5) an action add that adds an element to a set 6) an action is_empty returning true if a set is empty

Internally, the following are also used:

1) An unbounded sequence type index used for counting elements of basis 2) a relation disjoint beteween sets 3) a function card giving the cardinality of a set as an index 5) a function cnt gives the cardinality of the set of the set [0,x]

The index type is needed in order to represent the cardinality of the set [0,max], which is max+1 and thus cannot be represented by basis.

Note the functions above are stratified in this order: set -> basis -> index.t.

The implementation gives computable definitions of card, cnt and majority. The complexity of card and majority is quadratic in n, which is not optimimal but should be acceptable for small n (say, up to 5). In principal, we could add more efficient actions that compute these functions.

The main property provided is that any two majorities have an element in common. To prove this, we use a lemma stating that the sum of the cardinalities of two disjoint sets is <= cnt(n-1). This is proved by induction on n. This lemma implies the majority property.

include collections
include order
include deduction

The parameters of the module are:

  • basis: the basis object (instance of unbounded_sequence)
  • index: the index object (instance of unbounded_sequence)
module indexset(basis) = {

    type set 
    instance index : unbounded_sequence

    relation member(E:basis,S:set)
    function card(S:set) : index.t
    relation majority(S:set)

    action empty returns(s:set)
    action add(s:set,e:basis) returns (s:set)
    action is_empty(s:set) returns(r:bool)

    object spec = {

	after empty {
	    assert ~member(E,s);
	}

	after add {
	    assert member(X,s) <-> (member(X,old s) | X = e)
	}

	after is_empty {
	    assert r <-> ~ exists X. member(X,s)
	}
    }

    function cnt(E:basis) : index.t
    relation disjoint(X:set,Y:set)


    isolate disjointness = {

	object impl = {

The funtion cnt(x) returns the cardinality of the set of ordinals < x. We define it recursively by instantiating the recursion scheman fo the basis type.

Note here we use a definition schema. A definition of the form f(x:t) = ... is a shorthand for this schema:

# {
#     individual x :t
#     #----------------
#     property f(x) = ...
# }

The auto tactic will only unfold this definition schema for ground terms x occurring in the proof goal. Without this, we would exit the decidable fragment, due to a quantified variable under an arithmetic operator in the following definition.

	    definition cnt(x:basis) = 1 if x <= 0 else cnt(x-1) + 1
	    proof
                apply rec[basis]

We define cardinality in terms of a recursive function cardUpTo that counts the number of elements in a set that are less than or equal to a bound B.

	    function cardUpTo(S:set,B:basis) : index.t

Note that again the we use definition schema to stay decidable. Again, the rec[t] schema is used to admit a recursive definition.

	    definition cardUpTo(s:set,b:basis) =
		(1 if member(b,s) else 0) if b <= 0
                else (cardUpTo(s,b-1) + (1 if member(b,s) else 0))
	    proof
               apply rec[basis]

The cardinality function is then defined in terms of cardUpTo.

	    definition card(S) = cardUpTo(S,basis.max)

A majority is a set whose cardinality is greater than 1/2 of the basis set.

	    definition majority(X) = 2 * card(X) > cnt(basis.max)

	    object spec = {

This is the definition of dijoint sets in terms of the member relation. Notice that there is a quantifier alternation in the direction set -> basis.

		definition 
                    disjoint(X,Y) = forall E. ~(member(E,X) & member(E,Y))

This is our inductive invariant. It says that, for disjoint sets, the sum of the cardinalities up to bound B is less than the total number of elements less than B. We prove it by induction on B, using the induction schema for type basis. As usual, we have to giev the induction parameter explicitly, since Ivy can’t infer it automatically.

Most importantly, notice how arithmetic is used here. Because we used definition schemata, we never have an arithmetic applied to a universally quantified variable. This means our verification condition is is in the essentially uninterpreted fragment.

		property disjoint(X,Y) -> cardUpTo(X,B) + cardUpTo(Y,B) <= cnt(B)
		proof
                    apply ind[basis] with X = B;
                    showgoals
	    }

	}

	object spec = {

With the above lemma, Z3 can prove the “majorities intersect” property. The idea is that the lemma can be specialized to this:

 # property disjoint(X,Y) -> card(X) + card(Y) <= cnt(basis.max)

Since both majorities have cardinality greater than cnt(basis.max)/2, it follows that majorities cannot be disjoint, so they must have an element in common.

            property majority(X) & majority(Y) -> exists E. (member(E,X) & member(E,Y))
	}

	attribute test = impl
    }
    with basis.impl,index.impl

Note: we use the implementations of the basis and index types. That means both are interpreted. Fortunately, we don’t run afoul of the fragment checker.

    isolate api = {

	object impl = {

Here is the implementation of the set type using an unsorted array.

	    instance arridx : unbounded_sequence
	    instance arr:array(arridx,basis)

Tricky: this is a bit of aspect-orientation. It turns the type set into a struct with just one field called repr. This field gives the concrete representation of a set as an array. To an isolate that doesn’t use the definition of member below, the tpye set will still appear to be uninterpreted.

	    destructor repr(X:set) : arr.t

	    definition member(y:basis,X:set) = exists Z. 0 <= Z & Z < repr(X).end & repr(X).value(Z) = y

These lemmas are needed to prove correctness of is_empty.

	    property member(Y,X) -> repr(X).end ~= 0
	    property repr(X).end ~= 0 -> member(repr(X).value(0),X)

	    implement empty {
		repr(s) := arr.create(0,0)
	    }

	    implement add {
		if ~member(e,s) {
		    repr(s) := arr.resize(repr(s),repr(s).end.next,e)
		}
	    }

	    implement is_empty {
		r := repr(s).end = 0;
	    }
	}

	attribute test = impl

    } with spec

    attribute test = impl
}


The following is a test instantiation of indexset, to check that the verification goes through.

instance basis : unbounded_sequence
object basis = { ...
    var max : basis
}

instance nset : indexset(basis)

export nset.empty
export nset.add
export nset.is_empty