Leo Language Guide
Statically Typedβ
Leo is a statically typed language, which means we must know the type of each variable before executing a circuit.
Explicit Types Requiredβ
There is no undefined
or null
value in Leo. When assigning a new variable, the type of the value must be
explicitly stated.
Pass by Valueβ
Expressions in Leo are always passed by value, which means their values are always copied when they are used as function inputs or in right sides of assignments.
Data Types and Valuesβ
Booleansβ
Leo supports the traditional true
or false
boolean values. The explicit bool
type for booleans in statements is
required.
let b: bool = false;
Integersβ
Leo supports signed integer types i8
, i16
, i32
, i64
, i128
and unsigned integer types u8
, u16
, u32
, u64
, u128
.
let b: u8 = 1u8;
Underscores _
can be used to separate digits in integer literals.
let b: u8 = 1_000_000u64;
Higher bit length integers generate more constraints in the circuit, which can slow down computation time.
A Note on Leo Integersβ
Leo will not default to an integer type. The definition of an integer must include an explicit type.
Type casting is supported as of Leo v1.8.2
let a: u8 = 2u8; // explicit type
let b: u16 = a as u16; // type casting
let b: u8 = 2; // implicit type -- not supported
Field Elementsβ
Leo supports the field
type for elements of the base field of the elliptic curve.
These are unsigned integers less than the modulus of the base field. The following are the
smallest and largest field elements.
let a: field = 0field;
let b: field = 8444461749428370424248824938781546531375899335154063827935233455917409239040field;
Group Elementsβ
The set of affine points on the elliptic curve forms a group.
The curve is a Twisted Edwards curve with a = -1
and d = 3021
.
Leo supports a subgroup of the group, generated by a generator point, as a primitive data type.
A group element is denoted by the x-coordinate of its point; for example,
2group
means the point (2, 5553594316923449299484601589326170487897520766531075014687114064346375156608)
.
let a: group = 0group; // the point with 0 x-coordinate, (0, 1)
let b: group = 1540945439182663264862696551825005342995406165131907382295858612069623286213group; // the generator point
The aforementioned generator point can be obtained via a constant associated to the group
type.
let g: group = group::GEN; // the group generator
Scalar Elementsβ
Leo supports the scalar
type for elements of the scalar field defined by the elliptic curve subgroup.
These are unsigned integers less than the modulus of the scalar field. Showing the smallest and largest
scalars.
let a: scalar = 0scalar;
let b: scalar = 2111115437357092606062206234695386632838870926408408195193685246394721360382scalar;
Addressesβ
Addresses are defined to enable compiler-optimized routines for parsing and operating over addresses. These semantics will be accompanied by a standard library in a future sprint.
let receiver: address = aleo1ezamst4pjgj9zfxqq0fwfj8a4cjuqndmasgata3hggzqygggnyfq6kmyd4;
Signaturesβ
Aleo uses a Schnorr signatures scheme to sign messages with an Aleo private key.
Signatures in Leo have their own type signature
and can be declared as literals sign069ju4e8s66unu25celqycvsv3k9chdyz4n4sy62tx6wxj0u25vqp58hgu9hwyqc63qzxvjwesf2wz0krcvvw9kd9x0rsk4lwqn2acqhp9v0pdkhx6gvkanuuwratqmxa3du7l43c05253hhed9eg6ppzzfnjt06fpzp6msekdjxd36smjltndmxjndvv9x2uecsgngcwsc2qkns4afd
.
Signatures can be verified in Leo using the signature::verify
or s.verify
operators.
program test.aleo {
struct foo {
a: u8,
b: scalar
}
transition verify_field(s: signature, a: address, v: field) {
let first: bool = signature::verify(s, a, v);
let second: bool = s.verify(a, v);
assert_eq(first, second);
}
transition verify_foo(s: signature, a: address, v: foo) {
let first: bool = signature::verify(s, a, v);
let second: bool = s.verify(a, v);
assert_eq(first, second);
}
}
Layout of a Leo Programβ
A Leo program contains declarations of a Program Scope, Constants, Imports , Transition Functions, Helper Functions, Structs , Records, Mappings, and Finalize Functions. Declarations are locally accessible within a program file. If you need a declaration from another Leo file, you must import it.
Program Scopeβ
A program scope in the sense of Leo is a collection of code (its functions) and data (its types) that resides at a program ID on the Aleo blockchain.
import foo.leo;
program hello.aleo {
mapping balances: address => u64;
record token {
owner: address,
amount: u64,
}
struct message {
sender: address,
object: u64,
}
transition mint_public(
public receiver: address,
public amount: u64,
) -> token {
return token {
owner: receiver,
amount,
} then finalize(receiver, amount);
}
finalize mint_public(
public receiver: address,
public amount: u64,
) {
let current_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current_amount + amount);
}
function compute(a: u64, b: u64) -> u64 {
return a + b;
}
}
The following must be declared inside the program scope in a Leo file:
- constants
- mappings
- record types
- struct types
- transition functions
- helper functions
- finalize functions
The following must be declared outside the program scope in a Leo file:
- imports
Program IDβ
A program ID is declared as {name}.{network}
.
The first character of a name
must be lowercase.
name
can contain lowercase letters, numbers, and underscores.
Currently, aleo
is the only supported network
domain.
program hello.aleo; // valid
program Foo.aleo; // invalid
program baR.aleo; // invalid
program 0foo.aleo; // invalid
program 0_foo.aleo; // invalid
program _foo.aleo; // invalid
Constantβ
A constant is declared as const {name}: {type} = {expression};
.
Constants are immutable and must be assigned a value when declared.
Constants can be declared in the global program scope or in a local function scope.
program foo.aleo {
const FOO: u8 = 1u8;
function bar() -> u8 {
const BAR: u8 = 2u8;
return FOO + BAR;
}
}
Importβ
You can import dependencies that are downloaded to the imports
directory.
An import is declared as import {filename}.leo;
This will look for imports/{filename}.leo
and bring all declarations into the current file scope.
If there are duplicate names for declarations, Leo will fail to compile.
Ordering is enforced for imports, which must be at the top of file.
Leo imports are unstable and currently only provide minimal functionality. Their syntax is expected to change.
import foo.leo; // Import all `foo.leo` declarations into the `hello.aleo` program.
program hello.aleo { }
Structβ
A struct data type is declared as struct {name} {}
.
Structs contain component declarations {name}: {type},
.
struct array3 {
a0: u32,
a1: u32,
a2: u32,
}
Recordβ
A record data type is declared as record {name} {}
.
Records contain component declarations {visibility} {name}: {type},
.
A visibility can be either constant
, public
, or private
.
Users may also omit the visibility, in which case, Leo will default to private
.
Record data structures must contain the owner
component as shown below.
When passing a record as input to a program function, the _nonce: group
component is also required
(but it does not need to be declared in the Leo program).
record token {
// The token owner.
owner: address,
// The token amount.
amount: u64,
}
Arrayβ
Leo supports static arrays. Arrays are declared as [type; length]
and can be nested. Arrays cannot be empty nor modified.
Arrays only support constant accesses (the index must be a constant value). The accessor value must be a constant integer.
Arrays can contain primitive data types, structs, or arrays. Structs and records can also contain arrays.
Arrays can be iterated over using a for loop.
// Initalize a boolean array of length 4
let arr: [bool; 4] = [true, false, true, false];
// Nested Array
let nested: [[bool; 2]; 2] = [[true, false], [true, false]];
// Array of Structs
struct bar {
data: u8,
}
let arr_of_structs: [bar; 2] = [bar { data: 1u8 }, bar { data: 2u8 }];
// Access the field of a struct within an array
transition foo(a: [bar; 8]) -> u8 {
return a[0u8].data;
}
// Struct that contains an array
struct bat {
data: [u8; 8],
}
// Record that contains an array
record floo {
owner: address,
data: [u8; 8],
}
// Declare a mapping that contains an array value
mapping data: address => [bool; 8];
// Iterate over an array using a for loop and sum the values within
transition sum_with_loop(a: [u64; 4]) -> u64 {
let sum: u64 = 0u64;
for i: u8 in 0u8..4u8 {
sum += a[i];
}
return sum;
}
Tupleβ
Leo supports tuples. Tuples are declared as (type1, type2, ...)
and can be nested. Tuples cannot be empty nor modified.
Tuples only support constant access with a dot .
and a constant integer.
Tuples can contain primitive data types, structs, or arrays. Structs and records can also contain tuples.
program test.aleo {
transition baz(foo: u8, bar: u8) -> u8 {
let a: (u8, u8) = (foo, bar);
let result: u8 = a.0 + a.1;
return result;
}
}
Transition Functionβ
Transition functions in Leo are declared as transition {name}() {}
.
Transition functions can be called directly when running a Leo program (via leo run
).
Transition functions contain expressions and statements that can compute values.
Transition functions must be in a program's current scope to be called.
program hello.aleo {
transition foo(
public a: field,
b: field,
) -> field {
return a + b;
}
}
Function Inputsβ
A function input is declared as {visibility} {name}: {type},
Function inputs must be declared just after the function name declaration, in parentheses.
// The transition function `foo` takes a single input `a` with type `field` and visibility `public`.
transition foo(public a: field) { }
Function Outputsβ
A function output is calculated as return {expression};
.
Returning an output ends the execution of the function.
The return type of the function declaration must match the type of the returned {expression}
.
transition foo(public a: field) -> field {
// Returns the addition of the public input a and the value `1field`.
return a + 1field;
}
Helper Functionβ
A helper function is declared as function {name}() {}
.
Helper functions contain expressions and statements that can compute values,
however helper functions cannot produce records
.
Helper functions cannot be called directly from the outside. Instead, they must be called by other functions.
Inputs of helper functions cannot have {visibility}
modifiers like transition functions,
since they are used only internally, not as part of a program's external interface.
function foo(
a: field,
b: field,
) -> field {
return a + b;
}
Inline Functionβ
An inline function is declared as inline {name}() {}
.
Inline functions contain expressions and statements that can compute values.
Inline functions cannot be executed directly from the outside,
instead the Leo compiler inlines the body of the function at each call site.
Inputs of inline functions cannot have {visibility}
modifiers like transition functions,
since they are used only internally, not as part of a program's external interface.
inline foo(
a: field,
b: field,
) -> field {
return a + b;
}
The rules for functions (in the traditional sense) are as follows:
- There are three variants of functions: transition, function, inline.
- transitions can only call functions and inlines.
- functions can only call inlines.
- inlines can only call inlines.
- Direct/indirect recursive calls are not allowed
Finalize Functionβ
A finalize function is declared as finalize {name}:
and is used to run computations on chain. One of its primary purposes is to initiate or change public on chain state within mappings. A finalize function must immediately follow a transition function, and must have the same name;
it is associated with the transition function and is executed on chain,
after the zero-knowledge proof of the execution of the associated transition is verified;
a finalize function finalizes a transition function on chain.
Upon success of the finalize function, the program logic is executed.
Upon failure of the finalize function, the program logic is reverted.
Consequently, nodes on the Aleo network execute the code of the finalize function. Only code within finalize blocks, run by nodes on the Aleo Network, updates program mappings. Only a program can write into its own mapping, but all nodes on the Aleo network can read the public state.
An example of on-chain state mutation is the transfer_public_to_private transition in the finalize example, which updates the public account mapping (and thus a user's balance) when called.
program transfer.aleo {
// The function `transfer_public_to_private` turns a specified token amount
// from `account` into a token record for the specified receiver.
//
// This function preserves privacy for the receiver's record, however
// it publicly reveals the caller and the specified token amount.
transition transfer_public_to_private(
public receiver: address,
public amount: u64
) -> token {
// Produce a token record for the token receiver.
let new: token = token {
owner: receiver,
amount,
};
// Return the receiver's record, then decrement the token amount of the caller publicly.
return new then finalize(self.caller, amount);
}
finalize transfer_public_to_private(
public sender: address,
public amount: u64
) {
// Decrements `account[sender]` by `amount`.
// If `account[sender]` does not exist, it will be created.
// If `account[sender] - amount` underflows, `transfer_public_to_private` is reverted.
let current_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, current_amount - amount);
}
}
If there is no need to create or alter the public on-chain state, finalize functions are not required.
Mappingβ
A mapping is declared as mapping {name}: {key-type} => {value-type}
.
Mappings contain key-value pairs.
Mappings are stored on chain.
// On-chain storage of an `account` mapping,
// with `address` as the type of keys,
// and `u64` as the type of values.
mapping account: address => u64;
Mapping Operationsβ
The mapping struct allows the programmer to apply updates to a program mapping data structure by calling one of the following functions.
Mapping operations are only allowed in a finalize function.
program test.aleo {
mapping counter: address => u64;
transition dubble() {
return then finalize(self.caller);
}
finalize dubble(addr: address) {
let current_value: u64 = Mapping::get_or_use(counter, addr, 0u64);
Mapping::set(counter, addr, current_value + 1u64);
current_value = Mapping::get(counter, addr);
Mapping::set(counter, addr, current_value + 1u64);
}
}
getβ
A get command, e.g. current_value = Mapping::get(counter, addr);
Gets the value stored at addr
in counter
and stores the result in current_value
If the value at addr
does not exist, then the program will fail to execute.
get_or_useβ
A get command that uses the provided default in case of failure,
e.g. let current_value: u64 = Mapping::get_or_use(counter, addr, 0u64);
Gets the value stored at addr
in counter
and stores the result in current_value
.
If the key is not present, 0u64
is stored in counter
and stored in current_value
.
setβ
A set command, e.g. Mapping::set(counter, addr, current_value + 1u64);
Sets the addr
entry as current_value + 1u64
in counter
.
containsβ
A contains command, e.g. let contains: bool = Mapping::contains(counter, addr);
Returns true
if addr
is present in counter
, false
otherwise.
removeβ
A remove command, e.g. Mapping::remove(counter, addr);
Removes the entry at addr
in counter
.
For Loopsβ
For Loops are declared as for {variable: type} in {lower bound}..{upper bound}
. Unsigned integer
types u8
, u16
, and u32
are recommended for loop variable types. The lower bound must be
less than the upper bound. Nested loops are supported.
Exampleβ
let count: u32 = 0u32;
for i: u32 in 0u32..5u32 {
count += 1u32;
}
return count; // returns 5u32
Operatorsβ
Operators in Leo compute a value based off of one or more expressions. Leo will try to detect arithmetic operation errors as soon as possible. If an integer overflow or division by zero can be identified at compile time, Leo will quickly tell the programmer. Otherwise, the error will be caught at proving time when transition function inputs are processed.
For instance, addition adds first
with second
, storing the outcome in destination
.
For integer types, a constraint is added to check for overflow.
For cases where wrapping semantics are needed for integer types, see the Add Wrapped
operator.
let a: u8 = 1u8 + 1u8;
// a is equal to 2
a += 1u8;
// a is now equal to 3
a = a.add(1u8);
// a is now equal to 4
Arithmetic Operatorsβ
Operation | Operands | Supported Types |
---|---|---|
addition | + += .add() | field group scalar i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping addition | .add_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
negation(unary) | - .neg() | field group i8 i16 i32 i64 i128 |
subtraction(binary) | - -= .sub() | field group i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping subtraction(binary) | .sub_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
multiplication | * *= .mul() | field group scalar i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping multiplication | .mul_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
division | / /= .div() | field i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping division | .div_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
remainder | % %= .rem() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping remainder | .rem_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
exponentiation | ** **= .pow() | field i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping exponentiation | .pow_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
left shift | << <<= .shl() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping left shift | .shl_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
right shift | >> >>= .shr() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
wrapping right shift | .shr_wrapped() | i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
absolute value | .abs() | i8 i16 i32 i64 i128 |
wrapping absolute value | .abs_wrapped() | i8 i16 i32 i64 i128 |
doubling | .double() | field group |
squaring | .square() | field |
square root | .square_root() | field |
inverse | .square_root() | field |
Logical Operatorsβ
Operation | Operands | Supported Types |
---|---|---|
NOT | ! .not() | bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
AND | & &= .and() | bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
OR | | |= .or() | bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
XOR | ^ ^= .xor() | bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 |
NAND | .nand() | bool |
NOR | .nor() | bool |
conditional AND | && | bool |
conditional OR | || | bool |
Relational Operatorsβ
Relational operators will always resolve to a boolean bool
value.
Operation | Operands | Supported Types |
---|---|---|
equal | == .eq() | bool , group , field , integers, addresses, structs, records |
not-equal | != .neq() | bool , group , field , integers, addresses, structs, records |
less than | < .lt() | field , scalar , integers |
less than or equal | <= .lte() | field , scalar , integers |
greater than | > .gt() | field , scalar , integers |
greater than or equal | >= .gte() | field , scalar , integers |
Operator Precedenceβ
Operators will prioritize evaluation according to:
Operator | Associativity |
---|---|
! - (unary) | |
** | right to left |
* / | left to right |
+ - (binary) | left to right |
<< >> | left to right |
& | left to right |
| | left to right |
^ | left to right |
< > <= >= | |
== != | left to right |
&& | left to right |
|| | left to right |
= += -= *= /= %= **= <<= >>= &= |= ^= |
Parenthesesβ
To prioritize a different evaluation, use parentheses ()
around the expression.
let result = (a + 1u8) * 2u8;
(a + 1u8)
will be evaluated before multiplying by two * 2u8
.
Commandsβ
Leo supports several commands that can be used to reference information about the Aleo blockchain and the current transaction.
self.callerβ
self.caller
is currently implemented as self.signer
within Aleo instructions. This will be fixed in a upcoming release.
Returns the address of the account that is calling the program function.
program test.aleo {
transition matches(addr: address) -> bool {
return self.caller == addr;
}
}
block.heightβ
Returns the height of the current block.
block.height
is only allowed in a finalize function.
program test.aleo {
transition matches(height: u32) {
return then finalize(height);
} finalize matches(height: u32) {
assert_eq(height, block.height);
}
}
Core Functionsβ
Core functions are functions that are built into the Leo language. They are used to perform cryptographic operations such as hashing, commitment, and random number generation.
Assert and AssertEqβ
assert
and assert_eq
are used to verify that a condition is true.
If the condition is false, the program will fail.
program test.aleo {
transition matches() {
assert(true);
assert_eq(1u8, 1u8);
}
}
Hashβ
Leo supports the following hashing algorithms: BHP256
, BHP512
, BHP768
, BHP1024
, Pedersen64
, Pedersen128
, Poseidon2
, Poseidon4
, Poseidon8
, Keccak256
, Keccak384
, Keccak512
, SHA3_256
, SHA3_384
, SHA3_512
.
The output type of a commitment function is specified in the function name. e.g. hash_to_group
will return a group
type.
Hash functions take any type as an argument.
let a: scalar = BHP256::hash_to_scalar(1u8);
let b: address = Pedersen64::hash_to_address(1u128);
let c: group = Poseidon2::hash_to_group(1field);
Commitβ
Leo supports the following commitment algorithms: BHP256
, BHP512
, BHP768
, BHP1024
, Pedersen64
, Pedersen128
The output type of a commitment function is specified in the function name. e.g. commit_to_group
will return a group
type.
The first argument can be any type. The second argument must be a field
type and is used as a blinding factor.
let a: group = BHP256::commit_to_group(1u8, 2field);
let b: address = Pedersen64::commit_to_address(1u128, 2field);
Randomβ
Leo supports the ChaCha
random number generation algorithm.
The output type of a random function is specified in the function name. e.g. rand_group
will return a group
type.
Random functions are only allowed in a finalize function.
let a: group = ChaCha::rand_group();
let b: u32 = ChaCha::rand_u32();
Deprecated Syntaxβ
Increment and Decrementβ
increment()
and decrement()
functions are deprecated as of Leo v1.7.0.
Please use the Mapping::set()
function instead.
program transfer.aleo {
// On-chain storage of an `account` map,
// with `address` as the key,
// and `u64` as the value.
mapping account: address => u64;
transition transfer_public(...) {...}
finalize transfer_public(
public sender: address,
public receiver: address,
public amount: u64
) {
// Decrements `account[sender]` by `amount`.
// If `account[sender]` does not exist, it will be created.
// If `account[sender] - amount` underflows, `transfer_public` is reverted.
let sender_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, sender_amount - amount);
// Increments `account[receiver]` by `amount`.
// If `account[receiver]` does not exist, it will be created.
// If `account[receiver] + amount` overflows, `transfer_public` is reverted.
let receiver_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_amount + amount);
}
}