Aleo program deployment and operations
1. Overview
The full lifecycle of getting a Leo program onto the Aleo network: project setup, building, local networks, deployment, execution, upgrades, fees, and dependencies.
Version and canonical syntax
Target: Leo compiler >= 4.4.0. Consensus versions activate per network at a block height, so the two chains are rarely on the same rule set:
| Network | snarkOS release | Consensus version | Activation height |
|---|---|---|---|
| Mainnet | 4.9.0 | V18 | 20,794,000 |
| Mainnet | 4.9.1 | V19 | 21,342,000 |
| Testnet | 4.10.0 | V20 | 19,374,000 |
Query the target network for its active consensus version before you deploy. Testnet runs ahead of mainnet, so a rule you rehearsed on one chain may not be in force on the other.
Entry points are fn, state changes go in final { } blocks, and every deployable program carries a constructor with exactly one policy annotation. See aleo_smart_contracts for the language.
When these commands and the published docs disagree, trust leo --help on your installed version. The CLI has changed shape several times through the 4.x series.
2. Key concepts
leo runexecutes an entry point locally without generating a proof. It covers the proof half only;finalblocks do not run.leo executeexecutes locally, generates a proof, and builds a transaction. Add--broadcastto send it.leo deploycompiles and broadcasts a deployment transaction. It costs credits.leo upgradedeploys a new edition of an already-deployed program, if the constructor policy permits.- Program ID: the unique on-chain name, such as
my_token.aleo. Once taken on a network, it is taken permanently. - Edition: a version counter on a deployed program. Starts at 0 and increments with each upgrade.
- Microcredit: one credit is 1,000,000 microcredits. Fees are denominated in microcredits.
- Proving and verifying keys: generated during
leo build, cached underbuild/. Proving keys are large. - Consensus version: the protocol rule set active at a given block height, and the heights are per network. V18 activated at mainnet height 20,794,000; testnet is already scheduled for V20.
3. Project setup
leo new my_project
cd my_project
my_project/
├── build/ # compiled artifacts, generated
├── src/
│ └── main.leo
├── tests/
│ └── test_my_project.leo
├── .env # gitignored by default
├── .gitignore
└── program.json
leo new generates a .gitignore that already excludes .env, build/, *.prover, and *.verifier. Leave it that way.
program.json
{
"program": "my_project.aleo",
"version": "0.1.0",
"description": "My Aleo program",
"license": "MIT",
"leo": "4.4.0",
"dependencies": null,
"dev_dependencies": null
}
The leo field pins the compiler version. Leo 4.4 warns when it does not match the installed toolchain, which is worth heeding: a program compiled by a different minor version can produce different bytecode.
.env
NETWORK=testnet
PRIVATE_KEY=APrivateKey1...
ENDPOINT=https://api.explorer.provable.com/v1
The public endpoint is /v1. Never commit a .env holding a real key; use a secrets manager or an HSM for anything that touches mainnet.
4. Build pipeline
leo build
Output lands in build/: main.aleo with the compiled Aleo instructions, an ABI describing input and output types per function, and the proving and verifying keys.
leo build prints the compiled size against the 2000 KB ceiling, and since Leo 4.4 it lists each imported program separately. That breakdown is worth reading before you deploy: it is common for an application's own code to be modest while an imported registry or router dominates the footprint you are paying to store.
leo run <fn> <inputs> # local, no proof, proof half only
leo execute <fn> <inputs> # local, with proof and transaction
leo synthesize # generate keys without deploying
leo abi # generate the ABI from bytecode
Every major command takes --json-output for structured results, which is the right interface for scripting and agent use:
leo build --json-output
leo execute mint "aleo1..." 100u64 --json-output
leo deploy --json-output
Formatting is a plugin now
leo fmt ships as a separate plugin and is not part of the base install. Running it without the plugin gives:
Error [ECLI0377045]: 'leo-fmt' not found. Install the plugin and ensure it is available on your PATH.
Check what you have with leo plugins. A CI pipeline that assumes leo fmt --check works will fail on a clean install, so either install the plugin explicitly in CI or drop the formatting gate. The same applies to leo-lsp, leo-example, and leo-debug.
5. Local networks
Two options, and the lighter one is usually right.
leo devnode runs a single node. It starts in seconds and lets you advance the ledger on demand, which suits iterating on a deploy-execute-query loop:
leo devnode start
leo devnode advance --blocks 10
leo devnet runs a multi-validator network when you need real consensus behaviour:
leo devnet --num-validators 4 --num-clients 2 --network testnet
leo devnet --install # fetch snarkOS if you do not have it
leo devnet --snarkos /path/to/snarkos # use a specific binary
leo devnet --consensus-heights 0 # pin activation heights for a custom devnet
--install will fetch snarkOS for you, and --snarkos-version pins which build. Useful extras: --tmux to lay the nodes out in panes, --storage to point at a state directory, and --rest-port and friends when the defaults collide with something already running.
Local key
Leo's own help publishes the devnode key:
NETWORK=testnet
PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH
ENDPOINT=http://localhost:3030
That key corresponds to aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px. It is published, so treat anything it signs as public. It must never appear in a configuration that can reach a live network.
6. Deployment
leo deploy --broadcast # to the endpoint in .env
leo deploy --broadcast --devnet # against a local devnet
leo deploy --broadcast --priority-fees 1000000 # microcredits
leo deploy --broadcast --fee-records <record> # pay privately from a record
leo deploy --broadcast --private-key APrivateKey1... # override .env
leo deploy --print # build the transaction, print it, do not send
leo deploy --save ./tx # write the transaction to a directory
--print and --save without --broadcast are the safe way to inspect a deployment before it costs anything.
Cost model
Total = storage cost + synthesis cost + constructor cost + namespace cost
Storage cost scales with compiled byte size. Synthesis cost scales with the constraint count of each function. Constructor cost covers executing the constructor. Namespace cost depends on how short the program name is:
| Name length | Approximate namespace fee |
|---|---|
| 10+ characters | ~1 credit |
| 5 to 9 characters | ~10 credits |
| 2 to 4 characters | ~1,000 credits |
| 1 character | ~10,000,000,000 credits |
Long names are cheap. This is deliberate: short names are a scarce resource and priced like one.
Deployment limits under V18 and V19
Consensus V18 added protocol-level deployment-density checks. A deployment carries program bytecode and verification material into consensus state, which makes it an unusually expensive object, and fees alone turned out to be a weak bound on how concentrated that activity can get. Under V18 the answer moved: a large application split across many programs could compile cleanly and still be rejected when its components were deployed close together.
V19 replaced that density policy with a fixed cap per deployment transaction: 2^22 variables and 2^22 constraints, or 4,194,304 of each. The ceiling no longer depends on what anyone else deployed two blocks earlier, so you can synthesize a release candidate, compare its counts against 4,194,304, and gate on that number in CI. Leo 4.4.2 reports the V19 limits and counts constant variables in the variable total; an older toolchain undercounts and will tell you that you have headroom you do not have.
Leo reports no per-transaction limit for V18, because V18 did not use this model. Pin --consensus-version to the version the target chain will actually be running rather than assuming the newest limit applies retroactively.
A local devnode that mines each deployment immediately hides all of this. Rehearse releases with realistic block production and the consensus version you expect on the target network, which leo deploy --consensus-version lets you pin.
V20 on testnet
Testnet snarkOS 4.10.0 activates ConsensusVersion::V20 at height 19,374,000. Two changes matter when you rehearse there. Testnet MAX_CERTIFICATES becomes 40, which puts the committee model closer to production. And leader-produced blocks now carry a spend_limit derived from a conservative certificate floor:
min_certificates = 2 * ((MAX_CERTIFICATES + 2) / 3)
With 40 certificates that gives 28, so a leader-produced block is budgeted as if only 28 certificates took part. A transaction that fits comfortably in an ordinary block can still miss inclusion in a leader-produced one.
None of this is live on mainnet. Query the active consensus version on the network you are deploying to instead of assuming testnet and mainnet move together.
Verify
leo query program my_project.aleo
leo query program my_project.aleo --mappings
leo query program my_project.aleo --mapping-value account "aleo1..."
leo query program my_project.aleo --edition 0
7. Execution
leo execute mint "aleo1..." 100u64 --broadcast
leo execute mint "aleo1..." 100u64 --broadcast --devnet
leo query transaction <transaction_id>
leo execute → proof generated → transaction broadcast → pending
→ accepted, included in a block, final block runs on-chain
→ or rejected: invalid proof, insufficient fee, or a failed final block
A rejected transaction still costs the base fee. That is the reason get_or_use exists.
Other useful queries:
leo query block --latest
leo query mempool
leo query committee
leo query stateroot
8. Upgrades
The constructor's annotation decides whether an upgrade is possible at all, and it is fixed at deployment.
leo query program my_project.aleo # read the current edition
leo upgrade --broadcast
An upgrade can change function logic and add new functions, records, structs, mappings, and storage.
An upgrade cannot change the program ID, existing function signatures, existing struct or record definitions, or existing mapping names. The constructor and its policy annotation cannot be modified or removed once deployed.
That last constraint is the one to think about before your first deployment rather than after. @noupgrade is a promise to your callers that the code they audited is the code that runs. @admin is a promise that one key can change it. Both are visible on-chain and neither can be revised later.
9. Dependencies
leo add credits.aleo --network
leo add my_lib.aleo --local ../my_lib
leo remove my_lib.aleo
Dependencies must be deployed before the programs that import them:
cd my_lib && leo deploy --broadcast
cd ../my_app && leo deploy --broadcast
Read the per-import size lines in leo build output before deploying a program with dependencies. An import you added for one helper function may be carrying its entire compiled body into your deployment cost.
Verify the program ID of every dependency you add from the network. credits.aleo is the native credits program; a lookalike name is a supply-chain attack with a very small diff.
10. Common deployment errors
| Error | Cause | Fix |
|---|---|---|
| "program already exists" | the program ID is taken on that network | Pick a different name |
| "insufficient base fee" | not enough credits | Add credits, or reduce program size |
| "constructor failure" | the constructor rejected the deployment or upgrade | Check that you satisfy the policy, for example that you are the @admin address |
| "missing dependency" | an imported program is not on the network | Deploy the dependency first |
| "consensus version mismatch" | the program uses features not yet active | Check the network's consensus version and pin --consensus-version |
| "invalid private key" | wrong format or missing key | Check PRIVATE_KEY in .env |
ECLI0377045 "'leo-fmt' not found" | the plugin is not installed | Install the plugin or drop the formatting step |
| Deployment rejected near an activation height | the per-transaction constraint cap or a version boundary | Check the counts against the active version's limit and retry after the boundary |
11. Pre-deployment checklist
leo buildsucceeds and the size report looks reasonable, including imports.leo testpasses.leo test --provepasses, which catches constraint problems the fast path hides.- Sample inputs validated:
leo runfor pure entry points,leo executefor anything with afinalblock. - The constructor policy is the one you want to live with permanently.
- The program name is available on the target network and long enough to avoid a namespace surprise.
- Dependencies are deployed and their program IDs are verified.
- Fee budget estimated from the build size report.
- The private key is in a secrets manager, not a file.
.envpoints at the intended network, and the devnode key is nowhere near it.
Write programs with aleo_smart_contracts. Test with aleo_testing. Integrate with aleo_frontend and aleo_backend. Start from working code in aleo_cookbook.
13. Agent deployment workflow
- Write the program following
aleo_smart_contracts. leo build, and read the size report.leo test, thenleo test --prove.leo run <fn> <inputs>for entry points with nofinalblock.leo devnode start.leo deploy --broadcast --devnet.leo execute <fn> <inputs> --broadcast --devnetfor anything that finalizes.leo query program <name> --mapping-value <map> <key>to confirm the state change landed.leo upgrade --broadcast --devnetif you are rehearsing an upgrade.- Point
.envat testnet and repeat before going near mainnet. - Use
leo deploy --printfirst on any network where a mistake costs real credits.