5.1 Composability
On blockchains, single programs living on their own are not usually the norm. In fact, part of what's interesting about developing on a blockchain is that, in theory, anyone can build their own applications leveraging other's tokens, state, features... Things get more interesting when multiple programs work together. This is what we usually call program composability.
Importing a program
On Aleo a transition from a program can call a transition from another program. Here is how it works in practice.
Let's have a first program example, arithmetic.aleo
, with a transition that computes the square value of an input.
leo new arithmetic
cd arithmetic
And let's update arithmentic/src/main.leo
with the following code:
program arithmetic.aleo {
transition square_u64(a: u64) -> u64 {
return a*a;
}
}
Now let's create another program, quadratic residue, that computes the quadratic residue of a number mod another one, using our first transition.
cd ..
leo new quadratic
cd quadratic
Now we need first to add the other program to our list of external dependencies:
leo add arithmetic --local ../arithmetic
local
argument here is the path to the local Leo folder of the project. If you want to use a program deployed on chain already, use instead:
leo add your_program_id.aleo --network testnet # Or mainnet depending on the network
Our source file for that program, quadratic/src/main.leo
will include the following content:
import arithmetic.aleo;
program quadratic.aleo {
transition residue(a: u64, n: u64) -> u64 {
let squared: u64 = arithmetic.aleo/square_u64(a);
return squared.mod(n);
}
}
First, notice the line on top, outside of the program code block.
import arithmetic.aleo;
Then, inside of the residue
transition, notice the external call:
let squared: u64 = arithmetic.aleo/square_u64(a);
As you can see, external transition calls have almost the same syntax as calling an internal function. The difference is the function identifier includes the imported program ID then the functon name, separated by a slash.
On Aleo there is no such thing as "call by address", as you would do in Solidity, at the moment this course is being written. This means it is not possible to depend dynamically on any arbitrary program implementing a certain interface, then providing that program address as one of the argument of the caller function.
Signer vs Caller
Within a transition you can get the address of the direct caller of the transition, wether it's a program address or a user account address.
transition get_caller() -> address {
let direct_caller: address = self.caller;
return direct_caller;
}
For instance, for the following chain of calls:
User → Program A → Program B
self.caller
within program B would be program A's address.self.signer
within program B would be user's account address.
Here is an example. The function direct_calls_only
defined below could only be called directly from a user would fail otherwise, if called from a program importing it.
transition direct_calls_only() {
let direct_caller: address = self.caller;
let transaction_signer: address = self.signer;
assert_eq(direct_caller, transaction_signer);
}
You can use a program ID directly in Leo to get its address, for instance for getting the address of my_program.aleo
:
let my_program_address: address = my_program.aleo;
External Records
Let's update our imported program so it includes a counter record, to count the amount of times it has been called from quadratic.aleo
already:
program arithemtic.aleo {
record Counter {
owner: address,
amount: u64
}
transition create() -> Counter {
let counter: Counter = Counter {
owner: self.signer,
amount: 0u64
};
return counter;
}
// Square function now includes a Counter record input/output.
transition square_u64(a: u64, in_counter: Counter) -> (u64, Counter) {
let out_counter: Counter = Counter {
owner: in_counter.owner,
amount: in_counter.amount + 1u64
};
return (a*a, out_counter);
}
}
Here's how quadratic.aleo
can now provide those external records
import arithmetic.aleo;
program quadratic.aleo {
transition residue(
a: u64, n: u64, in_counter: arithmetic.aleo/Counter
) -> (u64, arithmetic.aleo/Counter) {
let (squared, out_counter): (
u64, arithmetic.aleo/Counter
) = arithmetic.aleo/square_u64(a, arithmetic.aleo/Counter);
let out_residue: u64 = squared.mod(n);
return (out_residue, out_counter);
}
}
Notice that before every record name, the imported program ID is provided before the slash.
Having a external record as an output of a program does not mean it will necessarily consume that record. The record will only be consumed if it is used as an input of a transition of the initial program it was defined in.
Imported structs
When importing a program that defines struct
data types, make sure you redefine those structs in the importing program as this could lead to errors. Contrary to records, you don't add the program ID prior to the imported struct name.
Here's an example of a program defining and using a struct:
program arithemtic.aleo {
struct Point {
x: u64,
y: u64
}
transition add_points(a: Point, b: Point) -> Point {
let c: Point = Point {
x: a.x + b.x,
y: a.y + b.y
};
return c;
}
}
And here's how the importing program can use those structs:
import arithmetic.aleo;
program quadratic.aleo {
// Redefine the struct with the same name and fields
struct Point {
x: u64,
y: u64
}
// Use struct as a normal struct
transition double_point(a: Point) -> Point {
let doubled: Point = quadratic.aleo/add_points(a, a);
return doubled;
}
}
Call an Async Transition
Async transition calls are slightly different than the non-async calls that we just went discovered. The reason for this is that there are both the on-chain and off-chain executed code. For each of those, we can decide independently if the caller program's code must be run before/after the imported code.
To understand this let's explore the syntax through an example first. Our arithmetic program will now store the counter of times its been called using a mapping.
program arithemtic.aleo {
mapping counter: u8 => u64;
// Square function now includes a Counter record input/output.
async transition square_u64(a: u64) -> (u64, Future) {
let square_u64_future: Future = finalize_square_u64();
return (a*a, square_u64_future);
}
async function finalize_square_u64(){
let amount: u64 = counter.get_or_use(0u8, 0u64);
amount += 1u64;
counter.set(0u8, amount);
}
}
Let's see how to call it from the quadratic.aleo
program:
import arithmetic.aleo;
program quadratic.aleo {
async transition residue(a: u64, n: u64) -> (u64, Future) {
let (squared, square_u64_future): (
u64, Future
) = arithmetic.aleo/square_u64(a, arithmetic.aleo/Counter);
let out_residue: u64 = squared.mod(n);
let residue_future: Future = finalize_residue(square_u64_future);
return (out_residue, residue_future);
}
async function finalize_residue(square_u64_future: Future){
// Do anything...
square_u64_future.await(); // Await the future
// Do anything else...
}
}
As you can notice, the future returned by the async external call is passed as an argument to the on-chain executed function.
All async external calls future must be passed to an async function and awaited this way. They cannot be returned directly. If there are multiple async external calls, each must be passed and awaited.
async transition calls_2transitions() -> Future {
let foo_future: Future = external.aleo/foo();
let bar_future: Future = external.aleo/bar();
let out_future: Future = finalize_calls_2transitions(foo_future, bar_future);
return out_future;
}
async function finalize_calls_2transitions(future1: Future, future2: Future){
future1.await();
future2.await();
}
All the function instructions, mapping reads, writes etc... are sequentially executed in the same order as their future are awaited.
Notice that the futures can be awaited in a different orders than the off-chain execution are made. The following body for the finalize_calls_2transitions
function above would be valid as well:
future2.await();
future1.await();
Reading external mappings
While a mapping cannot be updated directly by another program, it is possible to read its value. Here's an example:
import arithmetic.aleo;
program quadratic.aleo {
async transition read_external_mapping() -> (Future) {
return finalize_read_external_mapping();
}
async function finalize_read_external_mapping(){
let count: u64 = arithmetic.aleo/counter.get(0u8);
assert_neq(count, 0u64);
}
}
You can use the contains
and get_or_use
mapping functions this way as well.
Last updated