3.6 Mappings
Although records are useful to store a private state accessible to a single user in our applications, we still don't have a way to share a public state between multiple users. This is exactly what mappings are made for.
Syntax
Mappings are key value store that hold public state shared for every users. They are defined at the root of the program block using the mapping keyword:
program mappings.aleo {
mapping accumulator: u8 => u64;
// key => value
// ...
}
As you can see the key and value are explicitly declared during definition of a mapping can be any type.
Let's see now how to update and read state from these mappings.
Public Shared State
Until now, we've always written our code inside transition
structure. It allows for users to execute this code privately, with potentially private inputs/outputs, and just publish a proof of the execution on chain. This proof can simply be verified by other users on the network, without requiring them to execute the code, or even know the input/outputs. Not only does this enable privacy but it also allows scalability since the code need to be executed just once.
Although this approach causes a problem: how to handle sharing a state between users.
On public programable ledgers like Ethereum, the smart contracts code needs to be executed by every validators on the network. While this does not have the advantages cited above, it's required for having a public state shared between the different users. This is why Aleo also includes such public executions.
Lets see how this works in practice.
First we're introducing a new type of control structure, different from transitions, that will include the code that will be executed publicly on chain:
program mappings.aleo {
mapping accumulator: u8 => u64;
// Notice this block starts with 'async function' instead of 'transition'
async function increment_state(){
// Read mapping state and increment it by one
// TO DO
}
}
We'll play around with this example where we want to count how many times a function has been called by any users.
Functions cannot be called directly by users. For instance this will cause an error:
leo execute increment_state
All inputs and outputs of async functions are public.
Now we can write a transition as we are used to, but need to include some additional elements:
program mappings.aleo {
mapping accumulator: u8 => u64;
async function increment_state(){
// Read mapping state and increment it by one
// TO DO
}
async transition increment() -> Future {
let increment_future: Future = increment_state();
return increment_future;
}
}
Let's try to understand what's happening here. We've added a transition that has no input, and that returns a new special type of output: a Future
.
The async transition is still executed and proven off chain by the signer. But when the transaction is approved on chain, the output Future of the transition is ran on chain by the validators. This means that here, the increment function will be executed once the transaction is included in the ledger.
While it's not required at all, let's move the function block under the transition block for clarity, since it will be executed after.
program mappings.aleo {
mapping accumulator: u8 => u64;
async transition increment() -> Future {
let increment_future: Future = increment_state();
return increment_future;
}
async function increment_state(){
// Read mapping state and increment it by one
// TO DO
}
}
If a function's execution is halted, the corresponding transition is fully reverted as if it was never executed in the first place. All records created by the transition won't exist anymore for instance. This could be for any reason: operation under/overflow, assert instruction failing...
Update a Mapping Value
To update a mapping value, at a specific key, you can simply use the set
function:
accumulator.set(key, value);
The function will succeed even if a value already exist at that key and will replace the former value with the new one.
Read
The first way you can read a mapping value is by using the get
function:
let key: u8 = 123u8;
let value: u64 = accumulator.get(key);
Although, this instruction will fail and revert the whole execution if there was no value previously set. Instead, you could use the get_or_use
function, to provide a default value and make sure the instruction will succeed. The default value will then be returned if no prior value was seet at that key:
let key: u8 = 123u8;
let default: u8 = 0u64;
let value: u64 = accumulator.get_or_use(key, default);
Delete
Use the remove
function to delete an entry at a specific key:
let key: u8 = 123u8;
accumulator.remove(key);
This instruction will not halt even if no element existed at that key.
Contains
You can check if an element exists at some key using the contains
function:
let key: u8 = 123u8;
let exists: bool = accumulator.contains(key);
Example
Now back to our counting example, let's add a call to get the current count, and increment it by one on each call. We'll store the counter at key 0u8
:
program mappings.aleo {
mapping accumulator: u8 => u64;
async transition increment() -> Future {
let increment_future: Future = increment_state();
return increment_future;
}
async function increment_state(){
let current_count: u64 = accumulator.get_or_use(0u8, 0u64); // Get current value, default 0
let new_count: u64 = current_count + 1u64;
accumulator.set(0u8, new_count);
}
}
And that's it, we have a function that counts how many times it has been called.
Now if we execute this transition:
leo execute increment
As you can see we have 0 constraints, since no code is ran off chain in the circuit, and the output is the future, represented by the program id, the function name along with its argument (here none).
Leo ✅ Compiled 'mappings.aleo' into Aleo instructions
⛓ Constraints
• 'mappings.aleo/increment' - 0 constraints (called 1 time)
➡️ Output
• {
program_id: mappings.aleo,
function_name: increment,
arguments: []
}
In next chapter, we'll have a look at a (almost exhaustive) list of all the operators available in Leo.
Last updated