# Distributed Programs Veil's main DSL describes a system as a set of global actions over a shared state. For message-passing protocols there is a more compact way: describe the **program each node runs**, and the **network** that connects the nodes. Veil compiles such a description into an ordinary Veil module, so `#model_check`, `#check_invariants`, `sat trace` and the rest work on it unchanged. ``` node program + network assumptions ──compile──▶ Veil module ──▶ model checking / verification ``` The compiled module is strict by construction: a node can only read and write its own variables, and can only learn about other nodes through messages. ## Example Leader election in a unidirectional ring (compare with the global-action version in [Examples/Tutorial/Ring.lean](../Examples/Tutorial/Ring.lean)): ```lean veil module Ring type node instantiate tot : TotalOrder node instantiate btwn : Between node open Between TotalOrder network (synchrony := async) message token (id : node) var leader : Bool := false program (self : node) { let nxt :| ∀ z, self ≠ nxt ∧ ((z ≠ self ∧ z ≠ nxt) → btw self nxt z) send (token self) to nxt repeat recv (token id) if id = self then leader := true else if le self id then let nxt :| ∀ z, self ≠ nxt ∧ ((z ≠ self ∧ z ≠ nxt) → btw self nxt z) send (token id) to nxt } safety [single_leader] leader L1 ∧ leader L2 → L1 = L2 invariant [leader_greatest] leader L → le N L invariant [receive_self_msg_only_if_greatest] token_net S L L → le N L invariant [no_bypass] token_net P D S ∧ btw S N D → le N S #gen_spec #model_check { node := Fin 4 } { } #check_invariants end Ring ``` Properties are stated over the *global* state: a node variable `leader : Bool` is the field `leader : node → Bool`, and the network is visible through generated fields such as `token_net` (see [The generated module](#the-generated-module)). ## Declarations The declarations come in this order: the module's types, theory and any extra state, then `network`, `message`s and `var`s, then `program`, then properties and verification commands. `program` generates the module's state, initial state and actions, so it comes after every state declaration and replaces `#gen_state` and `after_init`. ### `network` ```lean network (synchrony := async) (loss := reliable) (channels := set) (order := unordered) (faults := none) (complexity := false) ``` Every option is optional; the defaults are shown above. | Option | Values | Meaning | |---|---|---| | `synchrony` | `async` | Nodes take steps in any interleaving, and a message can be delivered at any time after it is sent. | | | `sync` | Lock-step rounds: in each round every running node takes one step, then all messages of the round are delivered together. | | `loss` | `reliable` | Messages are never lost. | | | `lossy` | The network may drop any message (`async`), or any subset of a round's messages (`sync`). | | `channels` | `set` | Identical messages in flight (same sender, receiver and content) are merged. This keeps the state finite and the verification conditions free of arithmetic. | | | `multiset` | Every copy is kept (`async` only). | | `order` | `unordered` | Messages in flight may be received in any order. | | | `fifo` | Each channel (sender to receiver) delivers messages in the order they were sent (`async` only; see [Ordering](#ordering)). | | `faults` | `none` | All nodes follow the program. | | | `crash`, `crash f` | Nodes may crash, at most `f` of them (any term, e.g. an `immutable individual` declared before `network`). See [Faults](#faults). | | | `byzantine`, `byzantine p` | The nodes satisfying `p` are Byzantine. Without `p`, the compiler declares a theory relation `byz : node → Bool`, given in the second argument of `#model_check`. | | `complexity` | `false`/`true` | Count messages: `net_msgs` (sent with `send`), `net_bcasts` (sent with `broadcast`) and, for `sync`, `net_rounds`. | `network` also enables the rest of the syntax on this page. Its keywords (`send`, `recv`, `to`, ...) are reserved only in modules that declare a network. For safety properties of an asynchronous system, loss usually makes no difference: a message that is never delivered is already a possible behaviour. It matters for termination (see the `termination` clause in the [Model Checker Guide](ModelChecker-Guide.md#deadlocks-and-termination)), and in synchronous networks, where delivery within the round is otherwise guaranteed. ### `message` ```lean message vote (r : round) (v : value) message ack ``` Declares a message kind and the types of its fields. A message also carries its sender, which the network fills in: nodes cannot forge it. Field types must be finite (an uninterpreted `type`, an `enum`, `Bool`, `Fin n`, ...), because receiving a message is an action parameterised by its fields, and the model checker enumerates action parameters. ### `var` ```lean var leader : Bool := false var heard : node → Bool := fun _ => false var input : value := * var best : node := self ``` A variable every node has its own copy of. The initial value may refer to the node as `self`, or be `*` (an arbitrary value, chosen independently for each node). Without an initial value, the variable starts at its type's default. ### `program` ```lean program (self : node) { ... } ``` The program every node runs, with `self` the node running it. Its body is a Veil action body (`require`, `let x :| P`, `if`, assignments, ...) extended with the statements below. Different roles are written with `if`, e.g. `if self = coordinator then ... else ...`. The [proof-binding forms](DSL-Reference.md#proof-bindings) are available in program steps too, including `require h : P`, `assume h : P`, `assert h : P`, and `let x :| h : P`. For example: ```lean let other : node :| h : other = self have : other = self := h ``` `if x :| h : P then ... else ...` also binds the witness and its proof in the then-branch, with the same meaning as in an ordinary action. ## Statements | Statement | Meaning | |---|---| | `send (m a b) to dst` | Send message `m a b` to `dst`. | | `broadcast (m a b)` | Send it to every node, **including `self`**. | | `broadcast (m X) if c` | Broadcast `m x` for every `x` satisfying `c`, e.g. `broadcast (est V) if W V` sends every value in the node's set `W`. Fields written as capitalised variables range over all values, as in Veil assignments. The guard must be on the same line. Not available with `order := fifo`. | | `recv (m x y) from src` | Block until a message `m` arrives, then bind its fields and sender. `from src` is optional. | | `recv`
`\| (m x) from s => ...`
`\| (n y) => ...` | Block until one of several kinds of message arrives, and run the matching alternative. | | `next_round` | (`sync` only) End this node's step in the current round. | | `received (m a b) from src` | (`sync` only) A proposition: this node received `m a b` from `src` in the previous round. Without `from`, from some node. | | `halt` | Stop the node. Reaching the end of the program does the same. | | `while c do ...`, `repeat ...` | Loops. | In a `recv` pattern, a field written as a variable binds it, and a field written as any other term (e.g. `true`, an `enum` value, `r + 1`) only matches messages with that value. As in Lean's patterns, an identifier that names a constructor is a value, not a variable: ```lean recv | (vote true) from p => heard p := true | (vote false) from _p => aborted := true ``` `send`, `recv` and the other statements may appear at the top level of the program and inside `if`, `while`, `repeat` and `recv` alternatives, but not inside `match` or nested `do` blocks. `for`, `break` and `continue` are not supported. ## Semantics A node runs until it **blocks**: at a `recv`, at `next_round`, or when it halts. Everything a node does between two blocking points is one **atomic step**. Loop heads are step boundaries too, so a loop that never blocks still progresses one iteration per step rather than running forever inside one. * In an `async` network, any node that can take a step may do so, in any order. A node blocked at a `recv` can take a step when a matching message is in flight, and receiving removes that message. A `require` that does not hold blocks the node. * In a `sync` network, each round lets every node that has not halted take one step, ending at its next `next_round` (or `halt`). Once all have done so, all messages sent in the round are delivered at once, and become visible through `received` until the end of the next round. ### Faults * **Crash** (`faults := crash f`). At any time, the network may stop a node that is still running, at most `f` times. A crashed node takes no more steps; its `pc` is `pc_crashed`, and the generated `crashed n` holds. In an `async` network, the messages it sent before crashing stay in flight. In a `sync` network, a node that crashes during a round may have sent any subset of that round's messages: each of them may be dropped before the round ends. This is the usual model of FloodSet-style protocols. * **Byzantine** (`faults := byzantine p`). The nodes satisfying `p` do not run the program. Instead, at any time, they may send any message to any node, with their own identity as the sender (channels are authenticated). The predicate is part of the theory: in a model check, you choose which nodes are faulty, e.g. `{ byz := fun n => n = 2 }`; for SMT proofs, state what you assume about it, for instance with Veil's `ByzNodeSet`: ```lean instantiate bns : ByzNodeSet node nset open ByzNodeSet network (faults := byzantine is_byz) ``` Node programs cannot mention the predicate: nodes do not know who is faulty. Either way, the generated ghost relation `correct n` holds for the nodes that follow the program, so properties are usually stated over correct nodes: ```lean safety [agreement] correct A ∧ correct B ∧ decided A ∧ decided B → decision A = decision B ``` ### Ordering With `order := fifo`, every message carries a sequence number. The compiler declares a sort `net_seq` with `instantiate net_seq_order : TotalOrderWithMinimum net_seq`. Each channel keeps the number of the next message to send (`net_seq_sent src dst`) and the next one to receive (`net_seq_recv src dst`), and a `recv` can only take the next message of a channel. If that message does not match any alternative, the node waits, even if a matching message is queued behind it (head-of-line blocking). With `loss := lossy`, a `recv` may take any later message instead, and the ones it skips are lost; the messages that do arrive still arrive in order. Sequence numbers are an uninterpreted, totally ordered sort, so SMT proofs need no arithmetic. When model checking, instantiate it with a finite type, e.g. `{ node := Fin 2, net_seq := Fin 4 }`: sending a message needs a next sequence number, so this allows at most 3 messages per channel, and a node that would send a 4th blocks. ### Bindings do not survive blocking `let` bindings live only until the node blocks. A value that must survive a `recv`, `next_round` or a loop iteration belongs in a `var`: ```lean program (self : node) { let y := f self recv ping x := y -- error: `y` is not available here ... } ``` The compiler reports every use of an expired binding. ### Locality Inside `program`, every mention of a node variable `x` means `x self`, this node's copy. There is no way to name another node's copy, and the program may not mention any other mutable state: not the network's state, nor mutable state declared outside the program. Immutable state (the background theory, e.g. `TotalOrder` or a topology) is shared knowledge and may be read freely. ## The generated module `program` generates the following, which properties, `sat trace` queries and counterexample traces refer to. To see the generated commands, write `#desugar_program` directly after the `program`: like `exact?`, it offers a suggestion that replaces the `program` with the plain Veil it compiles to. | Name | Kind | Meaning | |---|---|---| | `pc : node → pc_label` | state | Where each node is suspended. | | `pc_start`, `pc_done` | `pc_label` | Not started yet; halted. | | `pc_recv`, `pc_round`, `pc_loop` | `pc_label` | Blocked at the `k`-th `recv` or `next_round`, or at the head of the `k`-th loop. | | `x : node → T` | state | Node variable `x : T` (a `relation` if `T` returns `Bool`). | | `m_net : node → node → T₁ → … → Bool` | state | (`async`) Messages `m` in flight, by sender and receiver. `→ Nat` (a count) with `channels := multiset`; with `order := fifo`, a `net_seq` argument follows the receiver. | | `m_out`, `m_in` | state | (`sync`) Messages `m` sent in the current round; delivered at the end of the previous one. | | `net_ready : node → Bool` | state | (`sync`) The node has not yet taken its step in this round. | | `net_msgs`, `net_bcasts`, `net_rounds` | state | (`complexity := true`) Message and round counts. | | `pc_crashed`, `net_crashes`, `net_crashing n` | `pc_label`, state | (`crash`) A crashed node; the number of crashes; `n` crashed in the current round (`sync`). | | `byz : node → Bool` | theory | (`byzantine` without a predicate) The Byzantine nodes. | | `net_seq`, `net_seq_order` | sort, instance | (`fifo`) Sequence numbers and their order. | | `net_seq_sent`, `net_seq_recv : node → node → net_seq` | state | (`fifo`) Next number to send, and to receive, on each channel. | | `halted n` | ghost | `pc n = pc_done`. | | `correct n` | ghost | `n` follows the program: not crashed (`crash`), not Byzantine (`byzantine`); `True` otherwise. | | `crashed n` | ghost | (`crash`) `pc n = pc_crashed`. | | `net_empty` | ghost | No message is in flight (`async`), or none has been sent in this round (`sync`). | | `node_start self` | action | The node's first step. | | `node_recv_ self src fields…` | action | Receive `m` at the `k`-th `recv`, and run until the next blocking point. | | `node_round self`, `node_loop self` | action | Resume after the `k`-th `next_round`, or at the head of the `k`-th loop. | | `net_drop_ src dst fields…` | action | (`lossy`) Lose a message in flight (`async`) or of the current round (`sync`); (`sync`, `crash`) lose a message of a node that crashed in this round. | | `net_byz_send_ src dst fields…` | action | (`byzantine`) A Byzantine node sends a message. | | `net_crash n` | action | (`crash`) Node `n` crashes. | | `net_next_round` | action | (`sync`) End the round and deliver its messages. | You may add further actions (e.g. failures, or an adversary) after `program`. They are ordinary Veil actions, and may use the generated state. ## Limitations * One program per module. Roles are written with `if`. * No partial synchrony and no duplicating networks yet. They, and other fault models, can be written as extra actions after `program`. * Byzantine nodes branch on every message they can send, at every step, so model check small instances. With `channels := multiset`, they can also make the state space infinite. * Message fields must have finite types. * Unbounded `Nat` variables or counters make the state space infinite unless the program terminates; bound the search with a `state_constraint` if needed. * `program` owns the module's initial state: mutable state declared outside it starts at its default value.