Skip to content

Commit 67ae9f8

Browse files
author
Mohammad Fawaz
committed
Dynamic dispatch example
1 parent 2c8312f commit 67ae9f8

17 files changed

Lines changed: 757 additions & 0 deletions

File tree

dynamic_dispatch/README.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Dynamic Dispatch in Leo
2+
3+
This example demonstrates three related Leo 4.0 features: **interfaces**, **dynamic dispatch**, and **dynamic records**.
4+
5+
## The Scenario
6+
7+
Different governance systems call for different voting-power formulas. A _linear_ strategy grants one vote per token (simple majority), while a _quadratic_ strategy grants floor(√tokens) votes (reducing whale influence). We want a single governance contract that can apply **either** formula — or any future formula — without being redeployed.
8+
9+
Dynamic dispatch makes this possible: the caller names the target strategy at runtime using the `identifier` type, and the AVM routes the call to whichever deployed program is named.
10+
11+
## Program Architecture
12+
13+
```
14+
voting_power.aleo quadratic_power.aleo
15+
│ declares VotingStrategy │ implements VotingStrategy
16+
│ implements (linear) │ (floor √balance votes)
17+
│ showcases dyn record │
18+
└──────────┬────────────────-┘
19+
│ governance.aleo imports voting_power.aleo
20+
│ to resolve the VotingStrategy interface,
21+
▼ then dispatches to either program at runtime
22+
governance.aleo
23+
get_voting_power(strategy, balance)
24+
proposal_passes(strategy, for_bal, against_bal)
25+
compare_strategies(balance)
26+
```
27+
28+
## Features Showcased
29+
30+
### Interfaces
31+
32+
An `interface` specifies the functions a program must expose. It is a _compile-time_ concept: Leo verifies the implementing program satisfies the contract; no interface bytecode appears on-chain.
33+
34+
```leo
35+
// In voting_power.aleo
36+
interface VotingStrategy {
37+
fn compute_power(balance: u64) -> u64;
38+
}
39+
40+
program voting_power.aleo : VotingStrategy {
41+
fn compute_power(balance: u64) -> u64 { return balance; }
42+
...
43+
}
44+
45+
// In quadratic_power.aleo — same interface, different implementation
46+
program quadratic_power.aleo : VotingStrategy {
47+
fn compute_power(balance: u64) -> u64 { /* floor(√balance) */ ... }
48+
...
49+
}
50+
```
51+
52+
### Dynamic Dispatch
53+
54+
The `identifier` type holds a program name resolved at runtime. The call syntax is:
55+
56+
```
57+
qualifier.aleo::Interface@(target)/function(args)
58+
│ │
59+
│ └─ `identifier` value — resolved at runtime
60+
└─ program that declared the interface — fixed at compile time
61+
```
62+
63+
`governance.aleo` accepts `strategy: identifier` as a parameter and routes to whichever program the caller names:
64+
65+
```leo
66+
fn get_voting_power(strategy: identifier, balance: u64) -> u64 {
67+
return voting_power.aleo::VotingStrategy@(strategy)/compute_power(balance);
68+
}
69+
```
70+
71+
Identifier literals use single quotes and can appear inline when the target is known at compile time:
72+
73+
```leo
74+
fn compare_strategies(balance: u64) -> (u64, u64) {
75+
let linear: u64 = voting_power.aleo::VotingStrategy@('voting_power')/compute_power(balance);
76+
let quadratic: u64 = voting_power.aleo::VotingStrategy@('quadratic_power')/compute_power(balance);
77+
return (linear, quadratic);
78+
}
79+
```
80+
81+
### Dynamic Records
82+
83+
A `dyn record` erases the concrete record type, allowing generic field access without knowing the full schema at the call site. Fields are read with a required type annotation; the operation fails at runtime if the field is missing or has the wrong type.
84+
85+
```leo
86+
// In voting_power.aleo
87+
record VoterRecord {
88+
owner: address,
89+
balance: u64,
90+
epoch: u32,
91+
}
92+
93+
fn inspect_balance(vr: VoterRecord) -> u64 {
94+
let dr: dyn record = vr as dyn record;
95+
let bal: u64 = dr.balance; // type annotation required
96+
return bal;
97+
}
98+
```
99+
100+
This pattern is already useful within a single program. Future Leo versions will extend it to cross-program record passing, so governance.aleo can accept `VoterRecord`s from any token program — even ones deployed after governance itself — without recompilation.
101+
102+
## Project Structure
103+
104+
```
105+
dynamic_dispatch/
106+
├── run.sh
107+
├── voting_power/ # VotingStrategy interface + linear implementation
108+
│ ├── program.json
109+
│ └── src/
110+
│ └── main.leo
111+
├── quadratic_power/ # Quadratic implementation of VotingStrategy
112+
│ ├── program.json
113+
│ └── src/
114+
│ └── main.leo
115+
└── governance/ # Dynamic dispatch hub
116+
├── program.json # lists voting_power.aleo + quadratic_power.aleo as network deps
117+
└── src/
118+
└── main.leo
119+
```
120+
121+
**Note on interface scoping:** The `VotingStrategy` interface is declared at the top level of each program's `.leo` file (outside the `program {}` block). An interface defined in one program is not automatically exported to programs that depend on it via `program.json`; each program re-declares the interface with the matching signature, and Leo's type checker verifies the contract at compile time.
122+
123+
**Note on network dependencies:** `governance/program.json` lists `voting_power.aleo` and `quadratic_power.aleo` with `"location": "network"`. When you run `leo build` (or any command that triggers a build), Leo fetches the deployed bytecode from the devnode endpoint into `build/imports/`, making it available to the local VM for proof generation.
124+
125+
## Running the Example
126+
127+
### Part 1 — Local execution (no devnode)
128+
129+
Individual programs can be tested locally with `leo run`:
130+
131+
```bash
132+
cd voting_power
133+
leo run compute_power 10000u64 # → 10000 (linear: 1:1)
134+
leo run compute_power 100u64 # → 100
135+
136+
cd ../quadratic_power
137+
leo run compute_power 10000u64 # → 100 (quadratic: √10000)
138+
leo run compute_power 100u64 # → 10 (quadratic: √100)
139+
```
140+
141+
### Part 2 — Dynamic dispatch (requires `leo devnode`)
142+
143+
Dynamic calls require all three programs to be deployed. Start a local devnode in a **separate terminal** first:
144+
145+
```bash
146+
leo devnode start --private-key APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH
147+
```
148+
149+
Then run the full demo from the `dynamic_dispatch/` directory:
150+
151+
```bash
152+
./run.sh
153+
```
154+
155+
The script:
156+
1. Deploys `voting_power.aleo` — establishes the interface on-chain
157+
2. Deploys `quadratic_power.aleo` — registers a second implementation
158+
3. Deploys `governance.aleo` — the dispatch hub (holds no logic itself)
159+
4. Calls `get_voting_power` with `'voting_power'` → routes to linear
160+
5. Calls `get_voting_power` with `'quadratic_power'` → routes to quadratic
161+
6. Calls `proposal_passes` to show diverging outcomes under each strategy
162+
7. Calls `compare_strategies` to see both results side-by-side
163+
164+
### Expected outputs
165+
166+
| Function | Strategy | Balance | Result |
167+
|---|---|---|---|
168+
| `compute_power` | linear | 10 000 | 10 000 |
169+
| `compute_power` | quadratic | 10 000 | 100 |
170+
| `proposal_passes` | linear | 1 000 000 vs 10 000 | true (10 000× margin) |
171+
| `proposal_passes` | quadratic | 1 000 000 vs 10 000 | true (10× margin) |
172+
| `compare_strategies` | both | 10 000 | (10 000, 100) |
173+
174+
## Key Takeaways
175+
176+
- **Interfaces** are compile-time contracts — they enforce structure without on-chain overhead.
177+
- **Dynamic dispatch** lets a single contract target any compliant program at runtime; adding a new strategy requires only a new deployment, not a governance upgrade.
178+
- **`identifier` literals** (`'program_name'`) give you dynamic dispatch with a compile-time-known target when you want both flexibility and readability.
179+
- **`dyn record`** enables generic record inspection today and will enable cross-program record passing once support is complete.

dynamic_dispatch/governance/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
NETWORK=testnet
2+
PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH
3+
ENDPOINT=http://localhost:3030
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"program": "governance.aleo",
3+
"version": "0.1.0",
4+
"description": "Governance contract that routes voting power queries to any VotingStrategy implementation via dynamic dispatch.",
5+
"license": "MIT",
6+
"dependencies": [
7+
{ "name": "voting_power.aleo", "location": "network", "path": null },
8+
{ "name": "quadratic_power.aleo", "location": "network", "path": null }
9+
]
10+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// governance.aleo is the dynamic dispatch hub.
2+
//
3+
// It holds no voting logic itself — instead it delegates power computation
4+
// to whichever VotingStrategy program the caller names at runtime. Adding
5+
// a new strategy (e.g., a conviction-voting program) requires no changes
6+
// to this contract: deploy the new program, pass its name as `strategy`,
7+
// and governance.aleo routes to it automatically.
8+
//
9+
// Dynamic call syntax (unqualified — interface declared locally):
10+
// VotingStrategy@(strategy)/compute_power(balance)
11+
// ├─ VotingStrategy → the interface (must be declared in this file)
12+
// ├─ @(strategy) → the runtime target (an `identifier` value)
13+
// └─ /compute_power → the function to invoke on that target
14+
//
15+
// The interface is re-declared here so Leo's type checker can verify the
16+
// call signature. At the AVM level, the dispatch checks that the named
17+
// program has a matching `compute_power(u64) -> u64` function.
18+
interface VotingStrategy {
19+
fn compute_power(balance: u64) -> u64;
20+
}
21+
22+
program governance.aleo {
23+
// Returns the voting power for `balance` tokens under the given strategy.
24+
//
25+
// `strategy` is an `identifier` resolved at runtime — e.g.:
26+
// 'voting_power' → linear: 10 000 tokens → 10 000 votes
27+
// 'quadratic_power' → quadratic: 10 000 tokens → 100 votes
28+
fn get_voting_power(strategy: identifier, balance: u64) -> u64 {
29+
return VotingStrategy@(strategy)/compute_power(balance);
30+
}
31+
32+
// Returns true when the weighted "for" side outweighs the "against" side.
33+
//
34+
// The same balance figures yield very different margins depending on the
35+
// strategy: under linear voting a whale dominates; under quadratic voting
36+
// their edge is shrunk to the square root of the difference.
37+
fn proposal_passes(
38+
strategy: identifier,
39+
for_balance: u64,
40+
against_balance: u64
41+
) -> bool {
42+
let for_power: u64 = VotingStrategy@(strategy)/compute_power(for_balance);
43+
let against_power: u64 = VotingStrategy@(strategy)/compute_power(against_balance);
44+
return for_power > against_power;
45+
}
46+
47+
// Evaluates the same balance under both strategies and returns the results
48+
// side-by-side. This function uses *identifier literals* ('voting_power'
49+
// and 'quadratic_power') rather than a variable, showing that the target
50+
// can be fixed at compile time when desired.
51+
fn compare_strategies(balance: u64) -> (u64, u64) {
52+
let linear_power: u64 = VotingStrategy@('voting_power')/compute_power(balance);
53+
let quadratic_power: u64 = VotingStrategy@('quadratic_power')/compute_power(balance);
54+
return (linear_power, quadratic_power);
55+
}
56+
57+
@noupgrade
58+
constructor() {}
59+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
NETWORK=testnet
2+
PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH
3+
ENDPOINT=http://localhost:3030
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"program": "quadratic_power.aleo",
3+
"structs": [],
4+
"records": [],
5+
"mappings": [],
6+
"storage_variables": [],
7+
"functions": [
8+
{
9+
"name": "compute_power",
10+
"is_final": false,
11+
"inputs": [
12+
{
13+
"name": "balance",
14+
"ty": {
15+
"Plaintext": {
16+
"Primitive": {
17+
"UInt": "U64"
18+
}
19+
}
20+
},
21+
"mode": "None"
22+
}
23+
],
24+
"outputs": [
25+
{
26+
"ty": {
27+
"Plaintext": {
28+
"Primitive": {
29+
"UInt": "U64"
30+
}
31+
}
32+
},
33+
"mode": "None"
34+
}
35+
]
36+
}
37+
]
38+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
program quadratic_power.aleo;
2+
3+
function compute_power:
4+
input r0 as u64.private;
5+
div r0 2u64 into r1;
6+
add r1 1u64 into r2;
7+
is.eq r2 0u64 into r3;
8+
ternary r3 1u64 r2 into r4;
9+
div r0 r4 into r5;
10+
add r2 r5 into r6;
11+
div r6 2u64 into r7;
12+
lt r7 r2 into r8;
13+
ternary r8 r7 r2 into r9;
14+
is.eq r9 0u64 into r10;
15+
ternary r10 1u64 r9 into r11;
16+
div r0 r11 into r12;
17+
add r9 r12 into r13;
18+
div r13 2u64 into r14;
19+
lt r14 r9 into r15;
20+
ternary r15 r14 r9 into r16;
21+
is.eq r16 0u64 into r17;
22+
ternary r17 1u64 r16 into r18;
23+
div r0 r18 into r19;
24+
add r16 r19 into r20;
25+
div r20 2u64 into r21;
26+
lt r21 r16 into r22;
27+
ternary r22 r21 r16 into r23;
28+
is.eq r23 0u64 into r24;
29+
ternary r24 1u64 r23 into r25;
30+
div r0 r25 into r26;
31+
add r23 r26 into r27;
32+
div r27 2u64 into r28;
33+
lt r28 r23 into r29;
34+
ternary r29 r28 r23 into r30;
35+
is.eq r30 0u64 into r31;
36+
ternary r31 1u64 r30 into r32;
37+
div r0 r32 into r33;
38+
add r30 r33 into r34;
39+
div r34 2u64 into r35;
40+
lt r35 r30 into r36;
41+
ternary r36 r35 r30 into r37;
42+
is.eq r37 0u64 into r38;
43+
ternary r38 1u64 r37 into r39;
44+
div r0 r39 into r40;
45+
add r37 r40 into r41;
46+
div r41 2u64 into r42;
47+
lt r42 r37 into r43;
48+
ternary r43 r42 r37 into r44;
49+
is.eq r44 0u64 into r45;
50+
ternary r45 1u64 r44 into r46;
51+
div r0 r46 into r47;
52+
add r44 r47 into r48;
53+
div r48 2u64 into r49;
54+
lt r49 r44 into r50;
55+
ternary r50 r49 r44 into r51;
56+
is.eq r51 0u64 into r52;
57+
ternary r52 1u64 r51 into r53;
58+
div r0 r53 into r54;
59+
add r51 r54 into r55;
60+
div r55 2u64 into r56;
61+
lt r56 r51 into r57;
62+
ternary r57 r56 r51 into r58;
63+
output r58 as u64.private;
64+
65+
constructor:
66+
assert.eq edition 0u16;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"program": "quadratic_power.aleo",
3+
"version": "0.1.0",
4+
"description": "",
5+
"license": "",
6+
"leo": "3.5.0",
7+
"dependencies": null,
8+
"dev_dependencies": null
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"program": "quadratic_power.aleo",
3+
"version": "0.1.0",
4+
"description": "Quadratic voting strategy: floor(sqrt(balance)) votes per token.",
5+
"license": "MIT"
6+
}

0 commit comments

Comments
 (0)