3.3 Variables and Types
In the last two chapters we started to manipulates some of the objects that are available in the Leo programming language, but there are plenty more that we'll discover here.
In Leo, all variable types must be explicitly declared:
let setting: bool = true; // VALID, will compile
let setting = true; // INVALID, will cause an error at compile time
Some variable are constants which are declared on top of the program, outside of transitions at the very root of the project, using the const
keyword, as in the following example:
program variables.aleo {
const SETTING: bool = true;
/*
By convention we'll always use capital letters for constants
in this course, to distinguish them from other variables.
*/
transition test() -> bool {
return SETTING || false;
}
}
While those can never be updated (as for inputs), other variables can, and must be created inside code blocks such as transitions using the let
keyword (we'll go through the other kind of block structures in two chapters):
program variables.aleo {
transition test() -> bool {
let output: bool = true; // Declaring the variable
output = output || false; // Declaring the variable
// By convention we'll always use snake case for variables in this course.
return output;
}
}
All variables in Leo must have a value when decalred, and cannot be left empty. Null type does not exist on Aleo.
Now that you know the base rules for manipulating variables, let's discover the available types.
Types
Fields
We'll start with the most fundamental type. Since the underlying constraint system of Aleo is R1CS, programs are essentially represented as sets of linear equations over a field. This makes elements of this field very special. They are simply called fields, for short, and represent elements of base field of the elliptic curve that aleo accounts are based upon.
Every other types that are available on Aleo are derived from this one, in order for the instructions that involve those extra types to be reduced to linear equations over the base field.
Fields are the elements of with:
They can be used directly in Leo by postfixing field
to their integer representation in as follows:
let element: field = 12field;
The smallest field is 0field
, and the largest is = 8444461749428370424248824938781546531375899335154063827935233455917409239040field
.
Naturally, the operations + - * /
are the corresponding field operations.
Group
Group elements represent points from a specific sub-group of the group of points on the Aleo elliptic curve. This sub-group is generated by a generator point noted group::GEN
. Group element can be used in Leo by postfixing group
to their x-coordinate. For instance, 2group
represents the point .
let a: group = 0group; // The point (0, 1)
let b: group = a + 2group; // Equals (2, 555359431692344929948460158932617048789752076653107501468711406434637515660)
Scalar
The sub-group described above has order:
This means that when adding the generator with itself successively: , by definition, we end up reaching every element of the group, until reaching . This set of integer by which we can multiply is hence simply the field , called the set of scalar. We call their elements scalars and they can be used directly in Leo by postfixing scalar
to their integer representation in as follows: .
let element: scalar = 2scalar;
Address
Addresses are just another representation of group elements (more specifically, it is the bech32 representation). They're public key, used to identify users and programs uniquely on the chain:
let owner: address = aleo1hqrz7wukfv2mmxzuzwes0w44s2se2v98w840k6euncdm8mwfd5pq2dwjcy;
Two useful addresses should be noted:
// 0group as address:
let zero_address: address = aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc;
// 2group as address:
let two_address: address = aleo1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanmpl0;
The view key associated with zero_address
is 0scalar
since , while the view key associated with two_address
is unknown. Finding it would require solving the discrete logarithm problem.
The private key for both of those addresses is unknown as well. This makes both of those addresses "burn addresses" in practice: sending token to those address would make those tokens unspendable. zero_address
is a "verifiable burn address" since anyone can decrypt data encrypted with this public key using 0scalar
view key. On the contrary, two_address
is a "private burn address".
Signatures
Leo supports Schnorr signatures verification within a program. The signature type represent the result from signing a field using a private key. Here is an example of how to verify such a signature:
program variables.aleo {
transition verify_signature(s: signature, a: address, v: field) -> bool {
let valid: bool = signature::verify(s, a, v);
return valid;
}
}
Generating a signature is not possible within a program. The reason is you could instead generate that signature outside of the program, pass that signature as an argument and validate it using the function above.
Booleans
Leo supports the traditional true
and false
boolean values.
let b: bool = false;
Integers
In addition to field and scalar integers. Leo supports the traditional integers that are more commonly used in computer science. There are the unsigned integers: u8, u16, u32, u64, u128
which support integers from to included, for type ub
. These bounds are enforce by the VM itself. Let's see an example
let a: u8 = 1u8; // VALID declaration of a
let b: u8 = 2u8; // VALID declaration of b
let c: u8 = a - b; // INVALID declaration of c
This program will compile, although it will crash at execution time, when trying to generate a proof for any input. Same goes if the upper bond is violated.
Then there are the signed integers: i8, i16, i32, i64, i128
which support integers from to included, for type ib
.
let a: u8 = -2i8; // VALID declaration of a
let a: u8 = 3i8; // VALID declaration of a
Integer declaration can include underscores _
to improve readability:
let a: u64 = 1_000_000u64;
Note that higher bit-length integers increase circuit constraints, hence proving time.
Also it's worth mentioning that it's not possible to mix multiple integer types in expressions:
let c: u128 = 3u128 - 1u32; // INVALID operation
See Casting section below to understand how that problem can be solved.
Structs
Structs are data structures that are composed of keys and values. They are defined at the root of the program block, using the struct
keyword, and used in the rest of the program:
program variables.aleo {
// Struct definition, notice the absence of semi-colon
struct Date {
year: u16,
month: u8,
day: u8
}
// By convention in this course, structs will be name using CamelCase
transition get_null_date() -> Date {
// Instantiation of a struct
let null_date: Date = Date {
year: 1970u16,
month: 1u16,
day: 1u16
};
return null_date;
}
// Struct properties can be accessed using "." followed by attribute name
transition is_january(date: Date) -> bool {
return date.month == 1u16;
}
}
Structs can be nested:
struct Person {
id: field,
date_of_birth: Date
}
// Struct can be nested multiple times:
struct Couple {
person1: Person,
person2: Person
}
It's not possible to update a struct:
let today: Date = Date {
year: 2025u16,
month: 3u8,
day: 2u8
};
today.day = 3u8; // INVALID, structs cannot be updated
// Instead you could use:
let tomorrow: Date = Date {
year: today.year,
month: today.month,
day: today.day + 1u8
};
Arrays
Leo supports static arrays with a fixed size. Arrays must declare both their type and length:
let arr: [bool; 4] = [true, false, true, false];
Arrays can be nested:
let nested: [[bool; 2]; 2] = [[true, false], [true, false]];
Arrays can also contain structs and structs can contain arrays:
struct StuctOfArray {
array: [u8; 2],
}
struct Bar {
data: u8,
}
// ...
let arr_of_structs: [Bar; 2] = [Bar { data: 1u8 }, Bar { data: 2u8 }];
Elements in arrays can be accessed using constant indices:
transition foo(a: [u8; 8]) -> u8 {
return a[0u8];
}
transition foo2(a: [bar; 8]) -> u8 {
return a[0u8].data;
}
Arrays can also be iterated over using a for
loop, we'll come bacj to this in chapter 3.5:
transition sum_with_loop(a: [u64; 4]) -> u64 {
let sum: u64 = 0u64;
for i: u8 in 0u8..4u8 {
sum += a[i];
}
return sum;
}
Tuples
Leo also supports tuples, which are immutable collections of values with fixed types. Tuples must have at least one element:
program variables.aleo {
transition baz(foo: u8, bar: u8) -> u8 {
let a: (u8, u8) = (foo, bar);
let result: u8 = a.0 + a.1;
return result;
}
}
Each of these data types plays a crucial role in Leo programming, enabling expressive and efficient circuits.
Casting
Variables can be casted from one type to another using the following synthax:
let a: u32 = 142u16 as u32;
let burner: address = 0group as address;
Although it might throw an error when the casting is not possible:
let negative: i8 = -1i8;
let unsigned: u8 = negative as u8; // INVALID
// A negative integer cannot be casted as an unsigned integer
Another, more complexe, example:
let a: u128 = 2u128 ** 126u16; // 2^126
let f: field = a as field; // 2^126
let f_squared: field = f*f; // 2^252
let s = f_squared as scalar; // INVALID, 2^252 is bigger than the biggest scalar
let s = f_squared as u128; // INVALID, 2^252 is bigger than the biggest u128
And a last example:
let g: group = 1field as group; // INVALID
// There is no element on the curve which x-coordinate is equal to -1
You now know pretty much every data types you can use on Aleo, but one: records. Let's find out what they are in next chapter.
Last updated