verify

Formally verifies the specification, searching for safety and liveness violations.

Synopsis

provengo verify [--max-depth NUM] [--offline] <path-to-project>

Description

This sub-command performs a depth-first search (DFS) over the entire specification space, looking for violations of halt(message) statements (safety violations) and non-progress cycles (liveness violations). verify is exhaustive up to --max-depth: if a violation exists within that depth, verify will find it.

When violations are found (up to 10), verify writes an HTML report describing each one, including a counter-example trace: the sequence of events leading to the violation. Additionally, Provengo will exit with error code 1, so that CIs and agents can easily detect that a violation was found.

Safety Violations

A safety violation means that "something irreversibly bad" happens at on of the runs possible under the model. Consider a loan request processor, for example. Assume that a regulation prevents giving an underage person a loan. We can capture this requirement and verify against it using the following bthread:

bthread("No loans for under-age", function(){
    waitForAll( IS_CHILD_EVENT, LOAN_GRANTED );                    (1)
    halt("Regulatory violation: we just gave a loan to a child."); (2)
});
  1. Waits for the events marking that the a applicant is a child, and a loan being granted. The order of the events does not matter.

  2. Marks the run as having a safety violations, with human readable explanation of what the violation is.

Liveness Violations

Intuitively, a liveness violation is a run where something that must happen at some point never does. For example, a loan process that runs - possibly forever - without concluding whether the loan request is approved or rejected. More formally, a liveness violation is a reachable cycle in the specification’s state space in which some goal you require to recur is never met.

For example, the requirement "we should keep seeing a heartbeat event", verified against a BP model where a cycle exists that loops forever without ever producing such event, would generate a liveness violation.

A liveness requirement is expressed by marking a sync() call as hot: a b-thread that is "hot" at a given sync point is asserting that it must not remain stuck at that exact sync forever.

bthread("Expect a HEARTBEAT, again and again", function () {
    while (true) {
        sync({ waitFor: Event("HEARTBEAT") }, undefined, true); // isHot=true
    }
});

The Constraints library provides a non-blocking alternative with a somewhat nicer syntax. Note that this examples is not equivalent: it only requests that the RESET event would happen at least once, at some point in the future:

bthread("Expect a HEARTBEAT at some point", function () {
    Constraints.requires(Event("HEARTBEAT")).eventually();
});

If verify's DFS finds a reachable cycle in which this b-thread is hot at every point, it reports a liveness violation naming the b-thread and the cycle. In the HTML report, liveness violations get a liveness tag (instead of safety), and their counter-example trace highlights where the offending cycle starts and, for cycles longer than one step, where it loops back around.

This checks two flavors of hot-cycle by default: a single b-thread that’s hot throughout the whole cycle, and a b-thread still hot when the b-program ends.

Deadlocks

verify also checks for deadlocks by default — a state where some event is requested but every b-thread that could otherwise let it through is blocking it, so no event can legally be selected and the b-program is stuck. This needs no configuration; deadlocks found during the DFS are reported alongside safety and liveness violations, tagged deadlock in the HTML report.

Parameters

--max-depth NUM

Maximum DFS exploration depth. Defaults to the same value as analyze's --max-depth.

--random-seed NUM

Seed number for random number generation.

-o/--output-file PATH

Path to a directory where the verification report should be created.

--offline

Creates a report that does not use on-line resources. Increases file size, but allows full functionality in offline environments.

Example 1. Finding a safety violation

The below modle has a subtle bug - it allows withdrowals from an empty bank account:

bthread("Choose transaction", function () {
    while (true) {
        request([bp.Event("DEPOSIT", { amount: 10 }), bp.Event("WITHDRAW", { amount: 10 })]);
    }
});

bthread("Balance must never go negative", function () {
    let balance = 0;
    while (true) {
        let e = waitFor(any(/^(DEPOSIT|WITHDRAW)$/));
        if (e.name === "DEPOSIT") {
            balance += e.data.amount;
        } else {
            balance -= e.data.amount;
            if ( balance >= 0 ) {
                halt("Balance went negative: " + balance);
            }
        }
    }
});

provengo verify finds the violation and writes an HTML report listing the violation’s description and a counter-example trace of events that leads to a negative balance (e.g. [DEPOSIT → DEPOSIT → WITHDRAW → WITHDRAW → WITHDRAW] ). Each event in the trace can be expanded to see its data, and a "Show All Details" button toggles all events at once.

Example 2. Finding a liveness violation

The following model causes a liveness violation:

bthread("Emit TICK forever", function () {
    while (true) {
        request(bp.Event("TICK"));
    }
});

bthread("Expect a RESET, again and again", function () {
    while (true) {
        sync({ waitFor: bp.Event("RESET") }, undefined, true); // hot: must eventually happen
    }
});

Nothing ever requests RESET, so the reachable cycle where only TICK repeats never satisfies the "Expect a RESET" b-thread’s goal. Running provengo verify again finds this too: a violation tagged liveness, described as Hot run violation: b-threads {Expect a RESET, again and again} can each get to an infinite hot loop…​, with its trace marking the step where the bad cycle starts.

See also: analyze, which visualizes the specification space rather than searching it for violations.