Deduction example: majority
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
- 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 = {
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]
function cardUpTo(S:set,B:basis) : index.t
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]
definition card(S) = cardUpTo(S,basis.max)
definition majority(X) = 2 * card(X) > cnt(basis.max)
object spec = {
definition
disjoint(X,Y) = forall E. ~(member(E,X) & member(E,Y))
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 = {
# 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
isolate api = {
object impl = {
instance arridx : unbounded_sequence
instance arr:array(arridx,basis)
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
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