Appendix C.2: Constructing Solutions using the ABI

The Application Binary Interface (ABI) of a contract provides all the essential information needed to construct a solution for one or more predicates within the contract. Although you could perform this manually, the pint-abi crate makes it much more ergonomic in Rust. The pint-abi crate includes the gen_from_file macro, which automatically generates the modules, types, and builder methods required to accomplish this.

The gen_from_file Macro

Consider the following simple contract in Pint:

storage {
    s_x: int,
    s_y: bool,
    s_z: {int, b256},
    s_a: {bool, int}[2],
    s_u: MyUnion,
    m1: (int => b256),
    m2: (int => (int => {bool, int})),
}

union MyUnion = A(int) | B;

predicate MyPredicate(
    x: int,
    y: bool,
    z: {int, b256},
    a: {bool, int}[2],
    u: MyUnion,
) {
    // Check arguments
    constraint x == 42;
    constraint y == true;
    constraint z == {2, 0x1111111100000000111111110000000011111111000000001111111100000000};
    constraint a == [{true, 1}, {false, 2}];
    constraint u == MyUnion::A(3);

    let s_x = mut storage::s_x;
    let s_y = mut storage::s_y;
    let s_z = mut storage::s_z;
    let s_a = mut storage::s_a;
    let s_u = mut storage::s_u;
    let m1_42 = mut storage::m1[42];
    let m2_5_6 = mut storage::m2[5][6];

    // Update state
    constraint s_x' == 7;
    constraint s_y' == true;
    constraint s_z' == {8, 0x2222222200000000222222220000000022222222000000002222222200000000};
    constraint s_a' == [{false, 3}, {true, 4}];
    constraint s_u' == MyUnion::B;
    constraint m1_42' == 0x3333333300000000333333330000000033333333000000003333333300000000;
    constraint m2_5_6' == {true, 69};
}

If you have pint-abi added as dependency in your Rust project, you'll have access to the gen_from_file macro which can be called as follows:

#![allow(unused)]
fn main() {
pint_abi::gen_from_file! {
    abi: "abi_gen_example/out/debug/abi_gen_example-abi.json",
    contract: "abi_gen_example/out/debug/abi_gen_example.json",
}
}

The macro takes two arguments:

  1. abi: a path to the ABI of the contract in JSON format
  2. contract: a path to the bytecode of the contract in JSON format

The macro above expands to a set of modules, types, and builder methods that allow creating data for solutions directly using Rust types without having to do the encoding manually.

In order to construct a solution to a predicate, you need construct two vectors:

  1. A vector of all the predicate arguments.
  2. A vector of all the state mutations.

Predicate Arguments

In order to construct a vector of arguments for the predicate above, you can use the following syntax:

#![allow(unused)]
fn main() {
    let arguments = MyPredicate::Vars {
        x: 42,
        y: true,
        z: (2, [0x1111111100000000; 4]),
        a: [(true, 1), (false, 2)],
        u: MyUnion::A(3),
    };
}

The module MyPredicate (which clearly corresponds to the predicate MyPredicate in our contract above) and the struct Vars are readily available to us from the expansion of the gen_from_file macro. The fields of the struct Vars exactly match the list of parameters of MyPredicate, both in name and type.

Each predicate parameter Pint type has a corresponding Rust type as follows:

Pint TypeRust Type
inti64
boolbool
b256[i64, 4]
UnionEnum
TupleTuple
ArrayArray

State Mutations

In order to construct a vector of proposed state mutations for the contract above, you can use the following syntax:

#![allow(unused)]
fn main() {
    let state_mutations: Vec<essential_types::solution::Mutation> = storage::mutations()
        .s_x(7)
        .s_y(true)
        .s_z(|tup| tup._0(8)._1([0x2222222200000000; 4]))
        .s_a(|arr| {
            arr.entry(0, |tup| tup._0(false)._1(3))
                .entry(1, |tup| tup._0(true)._1(4))
        })
        .s_u(MyUnion::B)
        .m1(|map| map.entry(42, [0x3333333300000000; 4]))
        .m2(|map| map.entry(5, |map| map.entry(6, |tup| tup._0(true)._1(69))))
        .into();
}

The module storage and the function mutations() are readily available for us from the expansion of the gen_from_file macro. The resulting object is of type Vec<essential_types::solution::Mutation> from the essential_types crate.

Because all state mutations are optional, they need to be set individually using their own builder methods (unlike predicate arguments). For simple types like int, bool, b256, and unions, the syntax is self explanatory. Those types have corresponding Rust types as for predicate arguments:

Pint TypeRust Type
inti64
boolbool
b256[i64, 4]
UnionEnum

For more complex types like tuples, arrays, and storage maps, you need to provide closures that specify how to set each part of the compound type:

  1. For tuples, you have available the builder methods _0(..), _1(..), etc. that specify a value for each individual entry of the tuple. you may skip some of these methods if you do not want to propose any mutations to the corresponding tuple field.
  2. For arrays, you have the builder method entry(..) which takes an integer index and a value to set the array value at that particular index. Not all array indices need to be set.
  3. For storage maps, you also have the builder method entry(..) which takes a key and a value to set the map value at that particularly key. The types of the key and the value must match the types of the key and the value of the map.

Contract and Predicate Addresses

The expansion of the macro gen_from_file also provides the address of the contract and the address of each predicate as constants:

#![allow(unused)]
fn main() {
    let contract_address = ADDRESS;
    let my_predicate_address = MyPredicate::ADDRESS;
}

These constants can be used to construct solutions when specifying what predicate is being solved.

Producing Solutions

We now have everything we need to produce a solution. When working with the EssentialVM, a Solution object can be constructed as follows:

#![allow(unused)]
fn main() {
    let solution = essential_types::solution::Solution {
        data: vec![essential_types::solution::SolutionData {
            predicate_to_solve: MyPredicate::ADDRESS,
            decision_variables: arguments.into(),
            state_mutations,
        }],
    };
}

where we have used the address of the MyPredicate to specify which predicate to solve, the vector arguments to specify values for decision_variables (recall that we sometimes refer to predicate parameters as decision variables), and the vector state_mutations to specify the proposed state mutations.

Note that this solutions only has a single SolutionsData. In general, solutions may contain multiple SolutionData objects which can all be produced by following the steps above.

Storage Keys

It is sometimes useful to know the storage keys where a particular storage variable (or some of its parts) are stored. The expansion of the gen_from_file macro also provides the builder method storage::keys() which can be used as follows:

#![allow(unused)]
fn main() {
    let keys: Vec<essential_types::Key> = storage::keys()
        .s_x()
        .s_y()
        .s_z(|tup| tup._0()._1())
        .s_a(|arr| {
            arr.entry(0, |tup| tup._0()._1())
                .entry(1, |tup| tup._0()._1())
        })
        .s_u()
        .m1(|map| map.entry(42))
        .m2(|map| map.entry(5, |map| map.entry(6, |tup| tup._0()._1())))
        .into();
}

The method keys() is readily available in the module storage. Similarly to mutations(), the key(s) for each storage variable must be appended using the corresponding builder method and the syntax is fairly similarly to the builder methods for state mutations. The result is a vector of Keys which can be used to query the state for example.