NethermindEth/horus-checker

How to use loop invariants

Leonard-Pat opened this issue · 7 comments

I was wondering what invariant condition (and where) I need to add to a recursive function

func compute_sum(n: felt) -> (sum: felt) {

    if (n == 0) {
        // When 0 is reached, return 0.
        return (sum=0);
    }

    // @invariant n >= 0
    loop:
    let (sum) = compute_sum(n=n - 1);
    let new_sum = sum + n;
    
    return (sum=new_sum);
}

func main() {
    let (res) = compute_sum(n=10);
    return();
}

I'm unsure if we can write a better invariant than this. @Ferinko would know best. It seems the example is not very interesting without a postcondition describing what the compute_sum() function should do. It isn't necessary, but without it, Horus is not really doing much.

Moreover, according to @Ferinko, the invariant annotation and label don't do anything because there's no jumping. Take a look at this, which only works for small n:

// @pre n >= 0
// @pre n <= 15
// @post 2 * $Return.sum == n * (n + 1)
func compute_sum(n: felt) -> (sum: felt) {
    if (n == 0) {
        // When 0 is reached, return 0.
        return (sum=0);
    }

    let (sum) = compute_sum(n=n - 1);
    let new_sum = sum + n;

    return (sum=new_sum);
}

func main() {
    let (res) = compute_sum(n=10);
    return();
}
(horus37) mal@computer:~/pkgs/horus-checker/demos$ ./display.sh invariant.cairo
~~~~~~~~~~~~~~~{SOURCE}~~~~~~~~~~~~~~
// @pre n >= 0
// @pre n <= 15
// @post 2 * $Return.sum == n * (n + 1)
func compute_sum(n: felt) -> (sum: felt) {
    if (n == 0) {
        // When 0 is reached, return 0.
        return (sum=0);
    }

    let (sum) = compute_sum(n=n - 1);
    let new_sum = sum + n;

    return (sum=new_sum);
}

func main() {
    let (res) = compute_sum(n=10);
    return();
}

~~~~~~~~~~~~~~{RESULT}~~~~~~~~~~~~~~~

real    0m1.265s
user    0m1.198s
sys     0m0.068s
Warning: Horus is currently in the *alpha* stage. Please be aware of the
Warning: known issues: https://github.com/NethermindEth/horus-checker/issues

compute_sum
Verified

main [inlined]
Verified


real    0m1.640s
user    0m1.618s
sys     0m0.024s
~~~~~~~~~~~~~~{REVISION}~~~~~~~~~~~~~
3f2e0db (HEAD -> langfield/makerdao-frob, origin/langfield/makerdao-frob) dirty: Bump `horus-compile` version exclusive upper bound to 0.0.8

~~~~~~~~~~~~~~{FILENAME}~~~~~~~~~~~~~
invariant.cairo

This at least works, although from the timeout behavior, I'm fairly sure it's checking via bit-blasting. Any thoughts on how to make this work for larger n @Ferinko?

Edit: Okay it works the same even without the label and invariant. I had assumed this example complained about a loop without invariant before it was added.

It should be noted that our upper bound needs to be sufficiently small so that the product does not wrap around, since we are working in a finite field.

I am very confused by this example - what are we trying to show? There's no looping behaviour here, the loop: label is never jumped to and its annotation is a normal Cairo comment, not a Horus thing ::thinking::. (And the sum is computed recursively.)

And if we look at the original example by @Leonard-Pat that does use a Horus annotation of invariant, then Horus will split the reasoning into:

Pre(compute_sum) -> Invariant(loop) and Invariant(loop -> Post(compute_sum); unfortunately, n > 0 is too weak to compute Post(compute_sum) from unless n is severerly bounded and Horus can bruteforce it.

There's no looping behaviour here, the loop: label is never jumped to and its annotation is a normal Cairo comment, not a Horus thing 🤔. (And the sum is computed recursively.)

Okay that's a typo. I guess I didn't understand what is and is not considered a loop.

@Leonard-Pat Can you show us the code example you were running where you did get the "loop with no invariant" error?

The function i'm trying to test is as follows:

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.alloc import alloc
from starkware.cairo.common.memcpy import memcpy
from starkware.cairo.common.math import assert_not_zero, assert_le, assert_nn
from starkware.starknet.common.syscalls import call_contract
from starkware.cairo.common.bool import TRUE, FALSE


struct CallArray {
    to: felt,
    selector: felt,
    data_offset: felt,
    data_len: felt,
}

// @pre call_array_len >= 0
// @post call_array_len == 0
func execute_multicall{syscall_ptr: felt*}(
    call_array_len: felt, call_array: CallArray*, calldata: felt*
) -> (response_len: felt, response: felt*) {
    alloc_locals;
    if (call_array_len == 0) {
        let (response) = alloc();
        return (0, response);
    }

    // call recursively all previous calls
    let (response_len, response: felt*) = execute_multicall(call_array_len - 1, call_array, calldata);
    let last_call = call_array[call_array_len - 1];

    // call the last call
    with_attr error_message("multicall {call_array_len} failed") {
        let res = call_contract(
            contract_address=last_call.to,
            function_selector=last_call.selector,
            calldata_size=last_call.data_len,
            calldata=calldata + last_call.data_offset,
        );
    }

    // store response data
    assert [response + response_len] = res.retdata_size;
    memcpy(response + response_len + 1, res.retdata, res.retdata_size);
    return (response_len + res.retdata_size + 1, response);
}

I can't seem to add an invariant check, or run horus-checker on it without it throwing an error

Okay this was addressed on Discord: basically, the loopy behavior that requires an @invariant is in a library function that we don't have a pre-written spec for yet. We're working on supporting it but it may take a while as it's nontrivial.