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:
abi
: a path to the ABI of the contract in JSON formatcontract
: 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:
- A vector of all the predicate arguments.
- 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 Type | Rust Type |
---|---|
int | i64 |
bool | bool |
b256 | [i64, 4] |
Union | Enum |
Tuple | Tuple |
Array | Array |
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 Type | Rust Type |
---|---|
int | i64 |
bool | bool |
b256 | [i64, 4] |
Union | Enum |
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:
- 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. - 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. - 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
Key
s which can be used to query the state for example.