Skip to main content

The Asynchronous Programming Model

Background

The Leo asynchronous programming model enables users to update public on-chain data using a developer-friendly syntax.

The execution of on-chain code is treated as an async function call which returns a Future object. The execution of the on-chain state change occurs after validators verify the proof associated with the transaction.

Managing Public State

On-chain data is stored publicly in one of three data structures: mappings, storage variables, and storage vectors. Any logic that reads from or updates the state of these structures must be contained within an async function block as follows:

program first_public_state.aleo {
mapping accumulator: u8 => u64;
storage count: u8;
storage queue: [u8];

async function increment_state_onchain(){
let current_count: u64 = accumulator.get_or_use(0u8, 0u64); // Get current value, defaults to 0
let new_count: u64 = current_count + 1u64;
accumulator.set(0u8, new_count);
}

async function increment_count_onchain(){
let current_count: u8 = count.unwrap_or(0u8); // Get current value, defaults to 0
count = current_count + 1u8;
}

async function add_to_queue_onchain(val: u8){
queue.push(val); // Push to end of queue
}
}

However, as users can only call transition functions, the Future generated by an async function must be returned from a transition in order to be usable. Any transition that invokes this process must be annotated with the async keyword. There are also a few other nuances:

  • Async transitions can return additional data types in a tuple, including Records, along with a Future.
  • Only one Future can be returned.
  • If multiple types are returned, the Future must be the last type in the tuple.
program first_public_state.aleo {
mapping accumulator: u8 => u64;
storage count: u8;
storage queue: [u8];

//=============================================================
// MAPPING MODIFICATION
//=============================================================
async transition increment_accumulator() -> Future {
return increment_state_onchain();
}
async function increment_accumulator_onchain(){
let current_count: u64 = accumulator.get_or_use(0u8, 0u64); // Get current value, defaults to 0
let new_count: u64 = current_count + 1u64;
accumulator.set(0u8, new_count);
}

//=============================================================
// STORAGE VARIABLE MODIFICATION
//=============================================================
async transition increment_count() -> Future {
return increment_count_onchain();
}
async function increment_count_onchain(){
let current_count: u8 = count.unwrap_or(0u8); // Get current value, defaults to 0
count = current_count + 1u8;
}

//=============================================================
// STORAGE VECTOR MODIFICATION
//=============================================================
async transition add_to_queue(val: u8) -> Future {
return add_to_queue_onchain(val: u8);
}
async function add_to_queue_onchain(val: u8){
queue.push(val); // Push to end of queue
}
}

Leo also offers a shorthand for writing onchain code in the form of async blocks within async transition functions.

program first_public_state.aleo {
mapping accumulator: u8 => u64;
storage count: u8;
storage queue: [u8];

//=============================================================
// MAPPING MODIFICATION
//=============================================================
async transition increment_accumulator() -> Future {
let f : Future = async {
let current_count: u64 = accumulator.get_or_use(0u8, 0u64); // Get current value, defaults to 0
let new_count: u64 = current_count + 1u64;
accumulator.set(0u8, new_count);
}
return f;
}

//=============================================================
// STORAGE VARIABLE MODIFICATION
//=============================================================
async transition increment_count() -> Future {
let f : Future = async {
let current_count: u8 = count.unwrap_or(0u8); // Get current value, defaults to 0
count = current_count + 1u8;
}
return f;
}

//=============================================================
// STORAGE VECTOR MODIFICATION
//=============================================================
async transition add_to_queue(val: u8) -> Future {
let f : Future = async {
queue.push(val); // Push to end of queue
}
return f;
}
}

External Async Transitions

Leo enables developers to call external async transitions from imported programs in an async transition. A call to an async transition returns a Future which must be passed as inputs to an async function. These Futures must be composed inside of the async function using the await keyword, as shown in the example below.

import first_public_storage.aleo;

program second_public_storage.aleo {
mapping hashes: u8 => scalar;

async transition two_mappings(value: u8) -> Future {
let increment_future: Future = first_public_storage.aleo/increment();
return finalize_update_mapping(value, imported_future);
}

async function finalize_update_mapping(value: u8, imported_future: Future) {
imported_future.await();
let hash: scalar = BHP256::hash_to_scalar(value);
hashes.set(value, hash);
}
}

If using async blocks, you will need to call the external async transition outside the block and await the resulting Future within.

import first_public_storage.aleo;

program second_public_storage.aleo {
mapping hashes: u8 => scalar;

async transition two_mappings(value: u8) -> Future {
let increment_future: Future = first_public_storage.aleo/increment();
let f: Future = async {
imported_future.await();
let hash: scalar = BHP256::hash_to_scalar(value);
hashes.set(value, hash);
}
return f;
}
}

You can access the inputs to an external future using the following syntax:

let f = imported_program.aleo/some_function();
let value = f.0; // or f.1, f.2, f.3 and so on depending on the input index

Managing Both Public and Private State

Updating private state on Aleo utilizes off-chain proof generation to preserve the confidentiality of the user’s data and associated address. Therefore, Records cannot be created or consumed within the scope of async functions. However, Records can be used inside of the scope of async transitions. This is because transition and async transition functions are initially executed off-chain and are accompanied by proofs of correct execution which are subsequently verified by validators. Once the proof is verified, validators execute the code contained within a Future, which is solely defined by code within an async function.

Public StatePrivate State
Function Typeasync function, async blockasync transition or transition
Data Storagemapping, storagerecord
Visibilityeveryonevisible if you have the viewkey