What is Finalize?
Finalize blocks enable on-chain computation and state changes in Leo programs. While regular functions execute off-chain with private inputs, finalize functions run on-chain with public inputs and can modify mapping state.
The Final Pattern
Functions return Final to trigger on-chain finalization:
fn transfer_public(public receiver: address, public amount: u64) -> Final {
return final { finalize_transfer_public(self.caller, receiver, amount); };
}
The final { ... } block specifies which finalize function to call with which arguments.
Finalize Functions
Finalize functions are declared with the final fn keywords:
final fn finalize_transfer_public(
public sender: address,
public receiver: address,
public amount: u64
) {
// Update sender balance
let sender_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, sender_amount - amount);
// Update receiver balance
let receiver_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_amount + amount);
}
Finalize functions are declared outside the program scope and must have all parameters marked as public.
Complete Example
Here’s a complete token transfer with finalization:
program token.aleo {
mapping account: address => u64;
// Off-chain function that returns Final
fn transfer_public(
public receiver: address,
public amount: u64
) -> Final {
return final {
finalize_transfer_public(self.caller, receiver, amount)
};
}
}
// On-chain finalize function
final fn finalize_transfer_public(
public sender: address,
public receiver: address,
public amount: u64
) {
// Deduct from sender
let sender_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, sender_amount - amount);
// Add to receiver
let receiver_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_amount + amount);
}
Off-Chain + On-Chain Pattern
Combine private computation with public state changes:
program token.aleo {
mapping account: address => u64;
record token {
owner: address,
amount: u64,
}
// Off-chain: Create private record, trigger on-chain update
fn transfer_private_to_public(
sender: token,
public receiver: address,
public amount: u64
) -> (token, Final) {
// Private computation
let difference: u64 = sender.amount - amount;
let remaining: token = token {
owner: sender.owner,
amount: difference,
};
// Return private record + trigger finalize
return (remaining, final {
finalize_transfer_private_to_public(receiver, amount)
});
}
}
// On-chain: Update public state
final fn finalize_transfer_private_to_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);
}
Accessing Blockchain State
Finalize functions can access blockchain context:
final fn finalize_play() {
// Access current block height
assert(block.height <= 1000u32);
// Use randomness
assert(ChaCha::rand_bool());
// Update mappings
let winners: u8 = num_winners.get_or_use(0u8, 0u8);
num_winners.set(0u8, winners + 1u8);
}
Block Context
Available blockchain information:
final fn check_block_info() {
// Current block height
let height: u32 = block.height;
// Check time-based conditions
assert(block.height >= START_BLOCK);
assert(block.height <= END_BLOCK);
}
Randomness
Access on-chain randomness:
final fn use_randomness() {
// Random boolean
let coin_flip: bool = ChaCha::rand_bool();
assert(coin_flip);
// Use randomness for selection
if ChaCha::rand_bool() {
process_winner();
}
}
Mapping Operations in Finalize
Finalize functions are the only place where mappings can be modified:
final fn finalize_mint_public(
public receiver: address,
public amount: u64
) {
// Read from mapping
let current_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
// Write to mapping
Mapping::set(account, receiver, current_amount + amount);
}
See Mappings for detailed mapping operations.
Validation in Finalize
Use assertions to validate conditions:
final fn finalize_play() {
// Validate block height
assert(block.height <= 1000u32);
// Validate random condition
assert(ChaCha::rand_bool());
// Validate state
let winners: u8 = num_winners.get_or_use(0u8, 0u8);
assert(winners < 5u8);
// Update if all assertions pass
num_winners.set(0u8, winners + 1u8);
}
If any assertion in a finalize function fails, the entire transaction is reverted, including off-chain record creation.
Multiple Return Values with Final
Return multiple values along with Final:
fn play() -> (Ticket, Final) {
let ticket: Ticket = Ticket {
owner: self.caller,
};
return (ticket, final { finalize_play() });
}
fn transfer_public_to_private(
public receiver: address,
public amount: u64
) -> (token, Final) {
let transferred: token = token {
owner: receiver,
amount: amount,
};
return (transferred, final {
finalize_transfer_public_to_private(self.caller, amount)
});
}
Finalize Parameters
All finalize parameters must be marked as public:
// Correct: All parameters are public
final fn finalize_update(
public sender: address,
public amount: u64,
public timestamp: u64
) {
// Implementation
}
// Incorrect: Private parameters not allowed
final fn invalid_finalize(
sender: address, // Error: must be public
amount: u64 // Error: must be public
) {
// This won't compile
}
Passing Data to Finalize
Pass data from the off-chain function to the finalize function:
fn transfer_public(
public receiver: address,
public amount: u64
) -> Final {
// Pass self.caller, receiver, and amount to finalize
return final {
finalize_transfer_public(self.caller, receiver, amount)
};
}
final fn finalize_transfer_public(
public sender: address, // From self.caller
public receiver: address, // From parameter
public amount: u64 // From parameter
) {
// Use the passed values
let sender_balance: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, sender_balance - amount);
let receiver_balance: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_balance + amount);
}
Lottery Example
Complete lottery program with finalization:
program lottery.aleo {
// Track number of winners
mapping num_winners: u8 => u8;
// Lottery ticket record
record Ticket {
owner: address,
}
// Off-chain: Create ticket, trigger on-chain validation
fn play() -> (Ticket, Final) {
let ticket: Ticket = Ticket {
owner: self.caller,
};
return (ticket, final { finalize_play() });
}
}
// On-chain: Validate and update state
final fn finalize_play() {
// Check lottery hasn't expired
assert(block.height <= 1000u32);
// Randomly determine winner
assert(ChaCha::rand_bool());
// Check winner limit
let winners: u8 = num_winners.get_or_use(0u8, 0u8);
assert(winners < 5u8);
// Record winner
num_winners.set(0u8, winners + 1u8);
}
Best Practices
1. Minimize On-Chain Computation
// Good: Complex computation off-chain
fn calculate_and_store(data: [u32; 100]) -> Final {
let result: u64 = expensive_calculation(data); // Off-chain
return final { store_result(result) }; // On-chain
}
final fn store_result(public result: u64) {
Mapping::set(results, 0u8, result);
}
// Avoid: Heavy computation on-chain
final fn bad_practice(public data: [u32; 100]) {
let result: u64 = expensive_calculation(data); // Expensive on-chain
Mapping::set(results, 0u8, result);
}
2. Validate Early
final fn safe_transfer(
public sender: address,
public receiver: address,
public amount: u64
) {
// Validate first
let sender_balance: u64 = Mapping::get_or_use(account, sender, 0u64);
assert(sender_balance >= amount);
// Then update
Mapping::set(account, sender, sender_balance - amount);
let receiver_balance: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_balance + amount);
}
3. Atomic Updates
// Good: Both updates succeed or fail together
final fn atomic_swap(
public addr1: address,
public addr2: address,
public amount: u64
) {
let balance1: u64 = Mapping::get_or_use(account, addr1, 0u64);
let balance2: u64 = Mapping::get_or_use(account, addr2, 0u64);
// Validate both accounts
assert(balance1 >= amount);
assert(balance2 >= amount);
// Update both
Mapping::set(account, addr1, balance1 - amount);
Mapping::set(account, addr2, balance2 + amount);
}
4. Clear Error Conditions
final fn validated_operation(
public user: address,
public amount: u64
) {
// Clear validation with meaningful checks
assert(amount > 0u64); // Amount must be positive
let balance: u64 = Mapping::get_or_use(account, user, 0u64);
assert(balance >= amount); // Sufficient balance required
assert(balance - amount <= MAX_BALANCE); // Result within limits
Mapping::set(account, user, balance - amount);
}
Common Patterns
Conditional State Updates
final fn update_high_score(
public player: address,
public score: u64
) {
let current: u64 = Mapping::get_or_use(high_scores, player, 0u64);
// Only update if new score is higher
if score > current {
Mapping::set(high_scores, player, score);
}
}
Time-Based Logic
const AUCTION_END: u32 = 10000u32;
final fn finalize_bid(public bidder: address, public amount: u64) {
// Check auction is still active
assert(block.height < AUCTION_END);
// Process bid
let highest_bid: u64 = Mapping::get_or_use(bids, 0u8, 0u64);
assert(amount > highest_bid);
Mapping::set(bids, 0u8, amount);
Mapping::set(bidders, 0u8, bidder);
}
Rate Limiting
mapping last_action: address => u32;
final fn rate_limited_action(public user: address) {
let last: u32 = Mapping::get_or_use(last_action, user, 0u32);
// Require 100 blocks between actions
assert(block.height >= last + 100u32);
// Perform action
process_action(user);
// Update timestamp
Mapping::set(last_action, user, block.height);
}
Next Steps
Mappings Learn about on-chain storage
Functions Return Final from functions
Records Combine with private records
Programs Program structure overview