Skip to content

Releases: ProvableHQ/leo

v4.0.2

22 Apr 19:02
1344884

Choose a tag to compare

What's Changed

Full Changelog: v4.0.1...v4.0.2

v4.0.1

14 Apr 01:59
abe1e1e

Choose a tag to compare

What's Changed

Full Changelog: v4.0.0...v4.0.1

v4.0.0

31 Mar 19:35
d9207de

Choose a tag to compare

Leo 4.0 is a major release that streamlines the language model, introduces first-class library and interface support, and ships full dynamic dispatch. The on-chain execution model has been simplified, the syntax made more consistent, and the foundation laid for large-scale program composition on Aleo.


Unified fn Syntax

The biggest surface-level change in 4.0 is the unification of all function declarations under a single fn keyword. The old transition, function, inline, and async transition keywords are gone.

Leo 3.5 Leo 4.0
transition foo() fn foo() inside program {}
async transition foo() -> Future fn foo() -> Final inside program {}
function foo() fn foo() outside program {}
inline foo() fn foo() outside program {}
async function foo() final { ... } block
async { ... } final { ... }
f.await() f.run()
Future Final
@test script foo() @test fn foo()

Entry functions live inside program {} and form the public interface of the program. Helper functions live outside program {} and cannot produce records. Functions that need on-chain logic include a final { } block — everything inside it runs atomically on the network.

// Helper function — outside program {}
fn add(a: u64, b: u64) -> u64 {
    return a + b;
}

program counter.aleo {
    mapping tally: address => u64;

    // Entry function — inside program {}
    fn increment(amount: u64) -> Final {
        let sum: u64 = add(tally[self.caller], amount);
        return final {
            // On-chain logic
            tally[self.caller] += amount;
        };
    }
}

Interfaces

Leo 4.0 introduces interfaces — a compile-time mechanism for defining structural contracts over programs. An interface declares the functions, records, mappings, and storage that a conforming program must provide. Programs opt in with : InterfaceName.

// Declared outside program {}
interface Transfer {
    record Token;

    fn transfer(input: Token, to: address, amount: u64) -> Token;
}

interface Pausable {
    fn pause();
    fn unpause();
}

// Implements both interfaces
program my_token.aleo : Transfer + Pausable {
    record Token {
        owner: address,
        amount: u64,
    }

    fn transfer(input: Token, to: address, amount: u64) -> Token {
        return Token { owner: to, amount };
    }

    fn pause() { ... }
    fn unpause() { ... }
}

Interfaces support inheritance — an interface can extend one or more other interfaces:

interface Token : Transfer + Balances {
    fn mint(to: address, amount: u64);
}

Record requirements in interfaces can be structural (with optional .. to allow extra fields):

interface HasMemo {
    record Rec { owner: address, memo: u64, .. }
}

Dynamic Dispatch

Programs can now call other programs without knowing their concrete identity at compile time. Dynamic dispatch uses the Interface@(target)::method(args) syntax, where target is a value of the new identifier type. Identifier literals use single-quote syntax.

interface Counter {
    fn increment(amount: u64) -> u64;
}

program dispatcher.aleo {
    fn run(target: identifier, amount: u64) -> u64 {
        // Calls `increment` on whichever program `target` names
        return Counter@(target)::increment(amount);
    }
}

// Calling with a literal identifier
let target: identifier = 'my_counter';

Records passed through dynamic calls are typed as dyn record — a record whose structure is not known at compile time. Fields can be accessed by name and are resolved at runtime.

fn get_memo(rec: dyn record) -> u64 {
    return rec.memo; // Fails at runtime if the field is absent
}

Leo Libraries

Programs can now be packaged as libraries — reusable collections of constants, structs, and functions with no on-chain footprint. Libraries are created with leo new --library, use lib.leo instead of main.leo, and contain no program {} block.

// math_utils/src/lib.leo
const PI: field = 3141592653field;

struct Point {
    x: i64,
    y: i64,
}

fn distance(a: Point, b: Point) -> field {
    // ...
}

Libraries are referenced by path with :: — no import statement is needed, just a program.json dependency entry:

// Consumer program
fn move_point(p: math_utils::Point, dx: i64) -> math_utils::Point {
    return math_utils::Point { x: p.x + dx, y: p.y };
}

Libraries support submodules (math_utils::geometry::area) and generic functions (fn dot::[N: u32](a: Vector, b: Vector) -> field). They are fully inlined at compile time.


External Path Separator: /::

All external paths now use :: instead of /:

// Leo 3.5
token.aleo/transfer(to, amount)

// Leo 4.0
token.aleo::transfer(to, amount)

This applies to external function calls, external storage access (token.aleo::balances), and library item paths.


Removed: Scripts and leo debug

Script functions and leo debug have been removed. In Leo 3.5, @test script functions provided an interactive debugging entrypoint invoked via leo debug. This feature has been retired in 4.0. Tests are now written as @test fn functions inside program {} and run with leo test.

// Leo 3.5 — removed
@test script run() { ... }

// Leo 4.0
program my_program.aleo {
    @test fn run() { ... }
}

leo test Improvements

  • Proof generation is skipped by default. leo test no longer downloads snarkVM parameters or generates proofs unless --prove is explicitly passed, making test iteration dramatically faster.
  • Non-zero exit status on failure. leo test now correctly exits with a non-zero code when any test fails, enabling proper CI integration.
leo test           # Fast — no proof generation
leo test --prove   # Full end-to-end proof generation

v3.5.0

09 Mar 20:06

Choose a tag to compare

Leo v3.5.0 — Highlights

Leo v3.5.0 introduces major improvements to the developer workflow, compiler infrastructure, and tooling ecosystem. This release adds formatting support, ABI generation, external storage access, a new devnode command, and major compiler improvements.

leo fmt — Code Formatter

Leo now includes a built-in formatter to automatically format Leo programs.

leo fmt

This formats all .leo files in your project.

Example:

Before

transition main(a:u32,b:u32)->u32{
return a+b;
}

After

transition main(a: u32, b: u32) -> u32 {
    return a + b;
}

JSON ABI Generation

leo build now generates a JSON ABI object that describes the interface of a Leo program. In addition, generating a JSON ABI object directly from a compiled .aleo program is also possible via leo abi:

leo abi program.aleo

Example JSON ABI:

{
  "program": "child1.aleo",
  "structs": [],
  "records": [],
  "mappings": [],
  "storage_variables": [],
  "transitions": [
    {
      "name": "main",
      "is_async": false,
      "inputs": [
        { "name": "b", "ty": { "Plaintext": { "Primitive": { "UInt": "U32" } } }, "mode": "None" }
      ],
      "outputs": [
        { "ty": { "Plaintext": { "Primitive": { "UInt": "U32" } } }, "mode": "None" }
      ]
    }
  ]
}

JSON ABIs help external tools that want to interface with Leo/Aleo programs, such as SDKs.

leo devnode — Local Development Node

A new command allows developers to quickly spin up a local Aleo devnode.

leo devnode

This simplifies local testing and removes the need to manually run and configure a node.

External Storage Access

Leo programs can now read storage from external programs.

Example:

let counter: u32? = token.aleo/counter;
let balance: u32? = token.aleo/balance.get(0);

CLI Shortcuts

Short commands are now available for common operations:

leo b   # build
leo r   # run
leo t   # test

Structured CLI Output

The CLI can now emit structured JSON output:

leo execute --json-output ...

Example output:

{
  "config": {
    "address": "XXXXXX",
    "network": "testnet",
    "endpoint": "http://localhost:3030",
    "consensus_version": 13
  },
  "program": "test_json_program.aleo",
  "function": "main",
  "outputs": [
    "3u32"
  ],
  "transaction_id": "XXXXXX",
  "stats": {
    "storage_cost": 1328,
    "execution_cost": 0,
    "priority_fee": 0,
    "total_cost": 1328
  },
  "broadcast": {
    "fee_id": "XXXXXX",
    "fee_transaction_id": "XXXXXX",
    "confirmed": true
  }
}

Signature Literals

Leo now supports signature literals and optional signatures.

Example:

let sig: signature = 0xsign1234...;

Optional signatures:

let sig: signature? = none;

Merged PRs

New Contributors

Read more

testnet-v3.5.1

02 Mar 15:22

Choose a tag to compare

testnet-v3.5.1 Pre-release
Pre-release
testnet v3.5.1

canary-v3.5.0

23 Feb 16:31
8e17f4c

Choose a tag to compare

canary-v3.5.0 Pre-release
Pre-release

What's Changed

Full Changelog: testnet-v3.5.0...canary-v3.5.0

testnet-v3.5.0

06 Feb 18:54
baca924

Choose a tag to compare

testnet-v3.5.0 Pre-release
Pre-release

What's Changed

New Contributors

Full Changelog: v3.3.1...testnet-v3.5.0

v3.4.0

02 Dec 21:30

Choose a tag to compare

Release Notes

Empty arrays and empty loop

Arrays of size 0 and loops over empty ranges are now supported in Leo. While these constructs do not produce any instructions in the compiled Aleo bytecode, they enable more generic or pattern-based programming styles in Leo, especially when writing code that abstracts over sizes, iterates conditionally, or uses compile-time parameters. For example:

inline build_default::[N: u32]() -> [u8; N] {
    let xs: [u8; N] = [0u8; N];
    // When N = 0 this loop is simply skipped
    for i:u32 in 0..N {
        xs[i] = 1u8;
    }
    return xs;
}

let xs = build_default::[0](); // yields []

Stability improvements

  • Improved identifier validation in Leo, resulting in clearer and more precise error messages, especially when Leo identifiers conflict with reserved Aleo identifiers.
  • Fixed an issue where local const values failed to compile correctly when used in loop ranges.
  • Resolved a crash in the common subexpression elimination optimization pass that occurred for certain patterns of function calls.

Breaking Changes

  • The *hash_native* core functions have been renamed to *hash_to_bits*.

v3.3.1

29 Oct 22:10

Choose a tag to compare

Patch

  • Fixes error in common subexpression elimination.

v3.3.0

28 Oct 20:11

Choose a tag to compare

Release Notes

Optional Types (T?)

Leo now supports first-class optional types using the T? syntax (e.g., u8?, Foo?, [u64?; 2]).
Optional values can be compared with none, assigned, passed into inline functions, and stored in arrays and structs.

program optionals.aleo {
    struct Point { x: u32, y: u32 }

    transition main() {
        // Optional integers
        let x: u8? = 42u8;
        let y = x.unwrap();        // Returns 42u8

        let z: u8? = none;
        let a = z.unwrap_or(99u8); // Returns 99u8

        // Array of optionals
        let arr: [u16?; 2] = [1u16, none];
        let first_val = arr[0].unwrap();         // Returns 1u16
        let second_val = arr[1].unwrap_or(0u16); // Returns 0u16

        // Optional struct
        let p: Point? = none;
        let p_val = p.unwrap_or(Point { x: 0u32, y: 0u32 }); // Returns default Point
    }
}

Type coercion from T to T? is supported in variable definitions, inline function calls, and intermediate expressions.
Explicit unwrapping is required to go from T? → T.


New Storage System

Leo now supports persistent storage variables and storage vectors using the storage keyword.
Storage variables and vectors are declared at program scope, similar to mappings.

program storage_ops.aleo {
    struct Point { x: field, y: field }

    storage counter: u32;        // singleton storage variable
    storage points: [Point];     // storage vector of `Point`s

    transition main() -> Future {
        return async {
            counter = 5u32;
            let old = counter.unwrap_or(0u32); // returns optional
            points.push(Point { x: 1field, y: 2field });
            let first = points.get(0u32).unwrap();
            points.set(0u32, Point { x: 3field, y: 4field });
            counter = none; // unset
        }
    }
}

Storage vector supported operations:

vec.push(10u32);         // Push 10u32 at the end of vector `vec`
let x = vec.pop();       // Pop and return the last element of `vec`
let y = vec.get(5);      // Get element at index 5
vec.set(3, 5u32);        // Set element at index 3 to `5u32`
let y = vec.len();       // Return the number of elements in `vec`
vec.swap_remove(3);      // Remove element at index 3 and replace with last

Internally, the compiler rewrites these high-level constructs into mappings and mapping operations.


ECDSA Signature Verification

ECDSA signature verification is now supported with 20 variants covering different hash algorithms and address formats.

// Verify with digest (pre-hashed message)
let valid: bool = ECDSA::verify_digest(sig, addr, digest);
let valid: bool = ECDSA::verify_digest_eth(sig, eth_addr, digest);

// Verify with Keccak256 hashing
let valid: bool = ECDSA::verify_keccak256(sig, addr, msg);
let valid: bool = ECDSA::verify_keccak256_raw(sig, addr, msg);
let valid: bool = ECDSA::verify_keccak256_eth(sig, eth_addr, msg);

Also available: keccak384, keccak512, sha3_256, sha3_384, sha3_512.

Parameters:

Parameter Type Description
sig [u8; 65] ECDSA signature (r, s, v)
addr [u8; 33] or [u8; 20] Public key or Ethereum-style address
digest [u8; 32] Pre-hashed message
msg Any Message to hash and verify

Raw Hash Operations

Leo now supports raw hash variants. These omit metadata and directly hash input bits, useful for interoperability with external (especially EVM) systems.
Inputs for raw variants of Keccak* and Sha3* must be byte-aligned (number of bits is a multiple of 8).

// Raw hash functions
let h: field = Keccak256::hash_to_field_raw(input);
let h: group = BHP256::hash_to_group_raw(input);
let h: address = Pedersen64::hash_to_address_raw(input);

// Native variants
let bits: [bool; 256] = Keccak256::hash_native(input);
let bits: [bool; 256] = Keccak256::hash_native_raw(input);

Available for:
BHP256, BHP512, BHP768, BHP1024, Pedersen64, Pedersen128, Poseidon2, Poseidon4, Poseidon8, Keccak256, Keccak384, Keccak512, SHA3_256, SHA3_384, SHA3_512.


Serialization / Deserialization Operations

Leo now supports serialize and deserialize operations to and from bits, with both metadata-inclusive and raw variants.
The compiler checks that bit sizes match.

// Standard serialization (includes metadata)
let bits: [bool; 58] = Serialize::to_bits(value);
let value: u32 = Deserialize::from_bits::[u32](bits);

// Raw serialization (no metadata)
let bits: [bool; 32] = Serialize::to_bits_raw(value);
let value: u32 = Deserialize::from_bits_raw::[u32](bits);

// Arrays
let bits: [bool; 128] = Serialize::to_bits_raw([1u32, 2u32, 3u32, 4u32]);
let arr: [u32; 4] = Deserialize::from_bits_raw::[[u32; 4]](bits);

Bit Lengths

Type Raw Bits Notes
u32 32
field 253
scalar 251
address 253
Non-raw + metadata overhead

leo synthesize Command

Generate proving and verifying keys for all transitions in a local or remote Leo program, along with circuit metadata:

leo synthesize credits.aleo --save keys \
  --endpoint https://api.explorer.provable.com/v1 \
  --network mainnet

Output includes:

  • Public inputs
  • Variables
  • Constraints
  • Non-zero entries in matrices
  • Circuit ID
  • Proving and verifying keys saved to disk

This enables better understanding of program size and key management.


Lossless Syntax Tree Parser

A new lossless syntax tree parser has been added.
While not user-facing yet, it lays the foundation for a future code formatter.


Common Subexpression Elimination (CSE)

New optimization pass reduces bytecode size by eliminating redundant expressions.


Enhanced Error Messages

Error messages now display multiple related source locations.
Example with duplicate struct members:

struct Person {
    name: field,
    age: u8,
    name: field,
}

Error:

Error [ETYC0372017]: the name `name` is defined multiple times in struct `Person`
    --> src/main.leo:3:9
   |
 3 |         name: field,
   |         ^^^^^^^^^^^ previous definition here
 ...
 5 |         name: field,
   |         ^^^^^^^^^^^ redefined here

Remaining Stability Improvements

  • Various fixes to the interpreter related to hashing correctness
  • Fixed broken leo query committee endpoint
  • Validates program names in leo new against reserved SnarkVM keywords
  • Enforces test isolation for native tests
  • Speeds up leo test by running native tests in parallel
  • Supports CheatCode::set_signer("APrivateKey1...") for test signer switching