Aleo Instructions Language Guide
Statically Typed
Aleo instructions 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 Aleo instructions. When assigning a new variable, the type of the value must be explicitly stated.
Pass by Value
Expressions in Aleo instructions 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.
Register based
There are no variable names in Aleo instructions.
All variables are stored in registers denoted rX
where X
is a non-negative whole number starting from 0 r0, r1, r2, etc.
.
Data Types and Values
Booleans
Aleo instructions supports the traditional true
or false
boolean values. The explicit boolean
type for booleans in statements is required.
function main:
input r0: boolean.private;
Integers
Aleo instructions supports signed integer types i8
, i16
, i32
, i64
, i128
and unsigned integer types u8
, u16
, u32
, u64
, u128
.
function main:
input r0: u8.public;
Higher bit length integers generate more constraints in the circuit, which can slow down computation time.
Field Elements
Aleo instructions 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, so the largest field element
is 8444461749428370424248824938781546531375899335154063827935233455917409239040field
.
function main:
input r0: field.private;
Group Elements
The set of affine points on the elliptic curve passed into the Aleo instructions compiler forms a group.
The curve is a Twisted Edwards curve with a = -1
and d = 3021
.
Aleo instructions 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)
.
The generator point is 1540945439182663264862696551825005342995406165131907382295858612069623286213group
.
function main:
input r0: group.private;
Scalar Elements
Aleo instructions 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, so the largest scalar is
2111115437357092606062206234695386632838870926408408195193685246394721360382scalar
.
function main:
input r0: scalar.private;
Addresses
Addresses are defined to enable compiler-optimized routines for parsing and operating over addresses.
function main:
input r0: address.private;
Signatures
Aleo uses a Schnorr signatures scheme to sign messages with an Aleo private key.
Signatures can be verified in Aleo instructions using the sign.verify
instruction.
sign.verify sign069ju4e8s66unu25celqycvsv3k9chdyz4n4sy62tx6wxj0u25vqp58hgu9hwyqc63qzxvjwesf2wz0krcvvw9kd9x0rsk4lwqn2acqhp9v0pdkhx6gvkanuuwratqmxa3du7l43c05253hhed9eg6ppzzfnjt06fpzp6msekdjxd36smjltndmxjndvv9x2uecsgngcwsc2qkns4afd r1 r2 into r3;
Layout of an Aleo Program
An Aleo program contains declarations of a Program ID, Imports, Functions, Closures, Structs, Records, Mappings, and Finalize. Ordering is only enforced for imports which must be at the top of file. Declarations are locally accessible within a program file. If you need a declaration from another program file, you must import it.
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
Import
An import is declared as import {ProgramID};
.
Imports fetch other declarations by their program ID and bring them into the current file scope.
You can import dependencies that are downloaded to the imports
directory.
import foo.aleo; // Import the `foo.aleo` program into the `hello.aleo` program.
program hello.aleo;
Function
A function is declared as function {name}:
.
Functions contain instructions that can compute values.
Functions must be in a program's current scope to be called.
function foo:
input r0 as field.public;
input r1 as field.private;
add r0 r1 into r2;
output r2 as field.private;
Function Inputs
A function input is declared as input {register} as {type}.{visibility};
.
Function inputs must be declared just after the function name declaration.
// The function `foo` takes a single input `r0` with type `field` and visibility `public`.
function foo:
input r0 as field.public;
Function Outputs
A function output is declared as output {register} as {type}.{visibility};
.
Function outputs must be declared at the end of the function definition.
...
output r0 as field.public;
Call a Function
In the Aleo protocol, calling a function creates a transition that can consume and produce records on-chain.
Use the aleo run
CLI command to pass inputs to a function and execute the program.
In Testnet3, program functions cannot call other internal program functions.
If you would like to develop "helper functions" that are called internally within a program, try writing a closure
.
Call an Imported Function
Aleo programs can externally call other Aleo programs using the call {program}/{function} {register} into {register}
instruction.
import foo.aleo;
program bar.aleo;
function call_external:
input r0 as u64.private;
call foo.aleo/baz r0 into r1; // Externally call function `baz` in foo.aleo with argument `r0` and store the result in `r1`.
output r1;
Closure
A closure is declared as closure {name}:
.
Closures contain instructions that can compute values.
Closures are helper functions that cannot be executed directly. Closures may be called by other functions.
closure foo:
input r0 as field;
input r1 as field;
add r0 r1 into r2;
output r2 as field;
Call a Closure
Aleo programs can internally call other Aleo closures using the call {name} {register} into {register}
instruction.
program bar.aleo;
function call_internal:
input r0 as u64.private;
call foo r0 into r1; // Internally call closure `foo` with argument `r0` and store the result in `r1`.
output r1;
Struct
A struct is a data type declared as struct {name}:
.
Structs contain component declarations {name} as {type}
.
struct array3:
a0 as u32;
a1 as u32;
a2 as u32;
To instantiate a struct
in a program use the cast
instruction.
function new_array3:
input r0 as u32.private;
input r1 as u32.private;
input r2 as u32.private;
cast r0 r1 r2 into r3 as array3;
output r3;
Array
An array is a data type declared as [{value}, {value}]
.
[true, false, true]
Arrays contain a list of values of the same type [{type}; {length}]
.
[boolean; 3u32]
Arrays can be initialized using the cast
opcode.
function new_array:
input r0 as boolean.private;
input r1 as boolean.private;
input r2 as boolean.private;
cast r0 r1 r2 into r3 as [boolean; 3u32];
output r3;
Arrays can be indexed using {name}[{index}]
.
function get_array_element:
input r0 as [boolean; 4u32].public;
input r1 as u32.public;
r0[r1] into r2;
output r2;
Arrays can be nested.
[[true, false, true, false], [false, true, false, true]]
function get_nested_array_element:
input r0 as [[boolean; 4u32]; 2u32].public;
r0[0u32][1u32] into r1;
output r1;
Aleo instructions currently only support fixed-length static arrays.
Record
A record type is declared as record {name}:
.
Records contain component declarations {name} as {type}.{visibility};
.
Record data structures must contain the owner
declaration as shown below.
When passing a record as input to a program function the _nonce as group.{visibility}
declaration is also required.
record token:
// The token owner.
owner as address.private;
// The token amount.
amount as u64.private;
To instantiate a record
in a program use the cast
instruction.
function new_token:
input r0 as address.private;
input r1 as u64.private;
input r2 as u64.private;
cast r0 r1 r2 into r3 as token.record;
output r3;
Mapping
A mapping is declared as mapping {name}:
.
Mappings contain key-value pairs.
Mappings must be defined within a program.
Mappings are stored publicly on-chain. It is not possible to store data privately in a mapping.
// On-chain storage of an `account` map, with `owner` as the key,
// and `amount` as the value.
mapping account:
// The token owner.
key owner as address.public;
// The token amount.
value amount as u64.public;
Contains
A contains command that checks if a key exists in a mapping, e.g. contains accounts[r0] into r1;
.
Get
A get command that retrieves a value from a mapping, e.g. get accounts[r0] into r1;
.
Get or Use
A get command that uses the provided default in case of failure, e.g. get.or_use accounts[r0] r1 into r2;
.
finalize transfer_public:
// Input the sender.
input r0 as address.public;
// Input the receiver.
input r1 as address.public;
// Input the amount.
input r2 as u64.public;
// Decrements `account[r0]` by `r2`.
// If `account[r0]` does not exist, 0u64 is used.
// If `account[r0] - r2` underflows, `transfer_public` is reverted.
get.or_use account[r0] 0u64 into r3;
sub r3 r2 into r4;
set r4 into account[r0];
// Increments `account[r1]` by `r2`.
// If `account[r1]` does not exist, 0u64 is used.
// If `account[r1] + r2` overflows, `transfer_public` is reverted.
get.or_use account[r1] 0u64 into r5;
add r5 r2 into r6;
set r6 into account[r1];
Set
A set command that sets a value in a mapping, e.g. set r0 into accounts[r0];
.
Remove
A remove command that removes a key-value pair from a mapping, e.g. remove accounts[r0];
.
Finalize
A finalize is declared as finalize {name}:
.
A finalize must immediately follow a function, and must have the same name;
// The `transfer_public_to_private` function turns a specified amount
// from the mapping `account` into a record for the specified receiver.
//
// This function preserves privacy for the receiver's record, however
// it publicly reveals the sender and the specified amount.
function transfer_public_to_private:
// Input the receiver.
input r0 as address.public;
// Input the amount.
input r1 as u64.public;
// Construct a record for the receiver.
cast r0 r1 into r2 as credits.record;
// Decrement the balance of the sender publicly.
async transfer_public_to_private self.caller r1 into r3;
// Output the record of the receiver.
output r2 as credits.record;
// Output the future of the decrement operation.
output r3 as token.aleo/transfer_public_to_private.future;
finalize transfer_public_to_private:
// Input the sender.
input r0 as address.public;
// Input the amount.
input r1 as u64.public;
// Retrieve the balance of the sender.
// If `account[r0]` does not exist, 0u64 is used.
get.or_use account[r0] 0u64 into r2;
// Decrements `account[r0]` by `r1`.
// If `r2 - r1` underflows, `trasfer_public_to_private` is reverted.
sub r2 r1 into r3;
// Updates the balance of the sender.
set r3 into account[r0];
Previously, a finalize
function was executed on chain after the zero-knowledge proof of the execution of the associated function is verified;
Upon success of the finalize function, the program logic was executed.
Upon failure of the finalize function, the program logic was reverted.
Futures
A future is equivalent to the call graph of the on-chain execution and is explicitly used when finalizing an execution. Instead of constructing the call graph implicitly from the code, the transition/circuit explicitly outputs a future, specifying which code blocks to run on-chain and how to run them.
future type
A user can declare a future type by specifying a Locator
followed by the tag .future
.
For example, credits.aleo/mint_public.future
.
A function
can only output a future and a finalize block can only take a future in as input.
A closure
cannot output a future or take a future in as input.
async call
A user can make an asynchronous call to the finalize block via the async
keyword.
For example, async mint_public r0 r1 into r2;
.
Note that the associated function must be specified.
This operation produces a Future
as output.
async
takes the place of the finalize
command, which was allowed in the body of a function after the output statements.
await command
A user can evaluate a future inside of a finalize block using the await
command.
For example, await r0;
.
An await
command can only be used in a finalize block.
The operand must be a register containing a Future.
Indexing a future.
A register containing a future can be indexed using the existing index syntax.
For example, r0[0u32]
.
This would get the input of the future at that specific index.
Accesses can be nested to match the nested structure of a future.
Future example
program basic_math.aleo;
mapping uses:
key user as address.public;
value count as i64.public;
function add_and_count:
input r0 as i64.private;
input r1 as i64.private;
add r0 r1 into r2;
async add_and_count self.caller into r3;
output r2 as i64.private;
output r3 as basic_math.aleo/add_and_count.future;
finalize add_and_count:
input r0 as address.public;
get.or_use uses[r0] 0i64 into r1;
add r1 1i64 into r2;
set r2 into uses[r0];
function sub_and_count:
input r0 as i64.private;
input r1 as i64.private;
sub r0 r1 into r2;
async sub_and_count self.caller into r3;
output r2 as i64.private;
output r3 as basic_math.aleo/sub_and_count.future;
finalize sub_and_count:
input r0 as address.public;
get.or_use uses[r0] 0i64 into r1;
add r1 1i64 into r2;
set r2 into uses[r0];
/////////////////////////////////////////////////
import basic_math.aleo;
program count_usages.aleo;
function add_and_subtract:
input r0 as i64.private;
input r1 as i64.private;
call basic_math.aleo/add_and_count r0 r1 into r2 r3;
call basic_math.aleo/sub_and_count r2 r1 into r4 r5;
assert.eq r0 r4;
assert.eq r3[0u32] r5[0u32];
async add_and_subtract r3 r5 into r6;
output r0 as i64.private;
output r6 as count_usages.aleo/add_and_subtract.future;
finalize add_and_subtract:
input r0 as basic_math.aleo/add_and_count.future;
input r1 as basic_math.aleo/sub_and_count.future;
await r0;
assert.eq r0[0u32] r1[0u32];
await r1;
There are a number of rules associated with using these components.
- If a function has a finalize block, it must have exactly one async instruction.
- If a function has a finalize block, it's last output must be a future.
- If a function does not have a finalize block, it cannot have an async instruction`.
- All futures created by calls need to be input to the async instruction in the order they were produced.
- An async call must reference the same function.
- All calls must be made before invoking async.
- The input futures types in a finalize block must match the order in which they were created in the function.
- All futures in a finalize must be await-ed and in the order in which they were specified.
- Instructions can be interleaved between invocations of call, async, and await.
Finalize Commands
The following commands are supported in Aleo Instructions to provide additional program functionality.
block.height
The block.height
command returns the height of the block in which the program is executed (latest block height + 1).
This can be useful for managing time-based access control to a program.
The block.height
command must be called within a finalize block.
assert.eq block.height 100u64;
rand.chacha
The rand.chacha
command returns a random number generated by the ChaCha20 algorithm.
This command supports sampling a random address
, boolean
, field
, group
, i8
, i16
, i32
, i64
, i128
, u8
, u16
, u32
, u64
, u128
, and scalar
.
Up to two additional seeds can be provided to the rand.chacha
command.
Currently, only ChaCha20 is supported, however, in the future, other random number generators may be supported.
rand.chacha into r0 as field;
rand.chacha r0 into r1 as field;
rand.chacha r0 r1 into r2 as field;
Hash
Aleo Instructions supports the following syntax for hashing to standard types.
hash.bhp256 r0 into r1 as address;
hash.bhp256 r0 into r1 as field;
hash.bhp256 r0 into r1 as group;
hash.bhp256 r0 into r1 as i8;
hash.bhp256 r0 into r1 as i16;
hash.bhp256 r0 into r1 as i32;
hash.bhp256 r0 into r1 as i64;
hash.bhp256 r0 into r1 as i128;
hash.bhp256 r0 into r1 as u8;
hash.bhp256 r0 into r1 as u16;
hash.bhp256 r0 into r1 as u32;
hash.bhp256 r0 into r1 as u64;
hash.bhp256 r0 into r1 as u128;
hash.bhp256 r0 into r1 as scalar;
hash.bhp512 ...;
hash.bhp768 ...;
hash.bhp1024 ...;
hash.ped64 ...;
hash.ped128 ...;
hash.psd2 ...;
hash.psd4 ...;
hash.psd8 ...;
Checkout the Aleo Instructions opcodes for a full list of supported hashing algorithms.
Commit
Aleo Instructions supports the following syntax for committing to standard types.
Note that the commit
command requires any type as the first argument, and a scalar
as the second argument.
commit.bhp256 r0 r1 into r2 as address;
commit.bhp256 r0 r1 into r2 as field;
commit.bhp256 r0 r1 into r2 as group;
commit.bhp512 ...;
commit.bhp768 ...;
commit.bhp1024 ...;
commit.ped64 ...;
commit.ped128 ...;
Checkout the Aleo Instructions opcodes for a full list of supported commitment algorithms.
position, branch.eq, branch.neq
The position
command, e.g. position exit
, indicates a point to branch execution to.
The branch.eq
command, e.g. branch.eq r0 r1 to exit
, which branches execution to the position indicated by exit
if r0
and r1
are equal.
The branch.neq
command, e.g. branch.neq r0 r1 to exit
, which branches execution to the position indicated by exit
if r0
and r1
are not equal.
Example The finalize block exits successfully if the input is 0u8 and fails otherwise.
program test_branch.aleo;
function run_test:
input r0 as u8.public;
finalize r0;
finalize run_test:
input r0 as u8.public;
branch.eq r0 0u8 to exit;
assert.eq true false;
position exit;
self.signer
The self.signer
command returns the user address that originated the transition.
This can be useful for managing access control to a program.
In the above example, the transfer_public_to_private
function decrements the balance of the sender publicly using self.signer
.
self.caller
The self.caller
command returns the address of the immediate caller of the program.
Program Interoperability
The examples in this section will use the following environment.
NETWORK=testnet
PRIVATE_KEY=APrivateKey1zkpE37QxQynZuEGg3XxYrTuvhzWbkVaN5NgzCdEGzS43Ms5 # user private key
ADDRESS=aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm # user address
Child and Parent Program
The following example demonstrates how a program parent.aleo
can call another program child.aleo
.
program child.aleo;
function foo:
output self.caller as address.public;
output self.signer as address.public;
import child.aleo;
program parent.aleo;
// Make an external program call from `parent.aleo` to `function foo` in `child.aleo`.
function foo:
call child.aleo/foo into r0 r1;
output r0 as address.public;
output r1 as address.public;
output self.caller as address.public;
output self.signer as address.public;
$ snarkvm execute foo
⛓ Constraints
• 'test.aleo/foo' - 2,025 constraints (called 1 time)
• 'child.aleo/foo' - 0 constraints (called 1 time)
➡️ Outputs
# The address of the caller of `child.aleo/foo` => `program.aleo`
• aleo18tpu6k9g6yvp7uudmee954vgsvffcegzez4y8v8pru0m6k6zdsqqw6mx3t
# The address that originated the sequence of calls leading up to `child.aleo/foo` => user address
• aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm
# The address of the caller of `program.aleo/foo` => user address
• aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm
# The address that originated the sequence of calls leading up to `program.aleo/foo` => user address
• aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm
User Callable Program
By asserting assert.eq self.caller self.signer;
on line 4, a developer can restrict their function such that it can only be called by users.
program child.aleo;
function foo:
assert.eq self.caller self.signer; // This check should fail if called by another program.
output self.caller as address.public;
output self.signer as address.public;
import child.aleo;
program parent.aleo;
// Make an external program call from `parent.aleo` to `function foo` in `child.aleo`.
function foo:
call child.aleo/foo into r0 r1;
output r0 as address.public;
output r1 as address.public;
output self.caller as address.public;
output self.signer as address.public;
$ snarkvm execute foo
⚠️ Failed to evaluate instruction (call child.aleo/foo into r0 r1;):
Failed to evaluate instruction (assert.eq self.caller self.signer ;):
'assert.eq' failed:
'aleo18tpu6k9g6yvp7uudmee954vgsvffcegzez4y8v8pru0m6k6zdsqqw6mx3t'
is not equal to
'aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm'
(should be equal)
Program Callable Program
By asserting assert.neq self.caller self.signer;
on line 4, a developer can restrict their function such that it can only be called by other programs.
program restrict.aleo;
function foo:
assert.neq self.caller self.signer;
output self.caller as address.public;
output self.signer as address.public;
$ snarkvm execute foo
⚠️ Failed to evaluate instruction (assert.neq self.caller self.signer ;):
'assert.neq' failed:
'aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm'
is equal to
'aleo1p2h0p8mr2pwrvd0llf2rz6gvtunya8alc49xldr8ajmk3p2c0sqs4fl5mm'
(should not be equal)