leo-lang 4.4.1

The Leo programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
// Copyright (C) 2019-2026 Provable Inc.
// This file is part of the Leo library.

// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.

use super::*;
use anyhow::{bail, ensure};
use itertools::Itertools;
use leo_ast::NetworkName;
use leo_package::fetch_from_network;
use snarkvm::prelude::{
    CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
    ConsensusVersion,
    MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
    TEST_CONSENSUS_VERSION_HEIGHTS,
    TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
};

pub const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com/v1";

/// Compiler Options wrapper for Build command. Also used by other commands which
/// require Build command output as their input.
#[derive(Parser, Clone, Debug, Default)]
pub struct BuildOptions {
    #[clap(long, help = "Build tests along with the main program and dependencies.")]
    pub build_tests: bool,
    #[clap(long, help = "Don't use the dependency cache.")]
    pub no_cache: bool,
    #[clap(long, help = "Don't use the local source code.")]
    pub no_local: bool,
    #[clap(long, help = "Resolve git dependencies from the lock file and local cache only; don't fetch from remotes.")]
    pub offline: bool,
    #[clap(
        long,
        help = "Print the program checksum and the checksum of each entry and view function (the `std::prog::function_checksum` targets)."
    )]
    pub checksums: bool,
    #[clap(skip)]
    pub no_std: bool,
}

/// Network connection overrides for the `.env` file. Flattened by every command that talks to a
/// network endpoint.
#[derive(Parser, Clone, Debug)]
pub struct EnvOptions {
    #[clap(
        long,
        help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file.",
        global = true
    )]
    pub(crate) network: Option<NetworkName>,
    #[clap(
        long,
        help = "The endpoint to deploy to. Overrides the `ENDPOINT` environment variable. We recommend using `https://api.explorer.provable.com/v1` for live networks and `http://localhost:3030` for local devnets.",
        global = true
    )]
    pub(crate) endpoint: Option<String>,
    #[clap(
        long,
        env = "NETWORK_RETRIES",
        help = "Number of times to retry a failed network request before giving up.",
        default_value = "2"
    )]
    pub(crate) network_retries: u32,
}

impl Default for EnvOptions {
    fn default() -> Self {
        Self { network: None, endpoint: None, network_retries: 2 }
    }
}

/// Private-key override, flattened by commands that sign or identify an account
/// (`run`, `devnode`, `deploy`, `execute`, `upgrade`).
#[derive(Parser, Clone, Debug, Default)]
pub struct PrivateKeyOptions {
    #[clap(
        long,
        help = "The private key to use for the deployment. Overrides the `PRIVATE_KEY` environment variable in your shell or `.env` file. We recommend using `APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH` for local devnets. This key should NEVER be used in production.",
        global = true
    )]
    pub(crate) private_key: Option<String>,
}

/// Consensus overrides, flattened only by transaction-producing commands (`deploy`, `execute`,
/// `upgrade`).
#[derive(Parser, Clone, Debug, Default)]
pub struct ConsensusOptions {
    #[clap(
        long,
        help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell."
    )]
    pub(crate) devnet: bool,
    #[clap(
        long,
        help = "Optional consensus heights to use. This should only be set if you are using a custom devnet.",
        value_delimiter = ','
    )]
    pub(crate) consensus_heights: Option<Vec<u32>>,
}

/// The fee options for the transactions.
#[derive(Parser, Clone, Debug, Default)]
pub struct FeeOptions {
    #[clap(
        long,
        help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
        value_delimiter = '|',
        value_parser = parse_amount
    )]
    pub(crate) priority_fees: Vec<Option<u64>>,
    #[clap(
        short,
        help = "Records to pay for fees privately, delimited by '|', and used in order. The fees must either be valid plaintext, ciphertext, or `default`. Defaults to public fees.",
        long,
        value_delimiter = '|',
        value_parser = parse_record_string,
    )]
    fee_records: Vec<Option<String>>,
}

// A helper function to parse amounts, which can either be a `u64` or `default`.
fn parse_amount(s: &str) -> Result<Option<u64>, String> {
    let trimmed = s.trim();
    if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
}

// A helper function to parse record strings, which can either be a string or `default`.
fn parse_record_string(s: &str) -> Result<Option<String>, String> {
    let trimmed = s.trim();
    if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
}

/// Parses the record string. If the string is a ciphertext, then attempt to decrypt it. Lifted from snarkOS.
fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
    match record.starts_with("record1") {
        true => {
            // Parse the ciphertext.
            let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
            // Derive the view key.
            let view_key = ViewKey::try_from(private_key)?;
            // Decrypt the ciphertext.
            Ok(ciphertext.decrypt(&view_key)?)
        }
        false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
    }
}

// A helper function to construct fee options for `k` transactions.
#[allow(clippy::type_complexity)]
pub fn parse_fee_options<N: Network>(
    private_key: &PrivateKey<N>,
    fee_options: &FeeOptions,
    k: usize,
) -> Result<Vec<(Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
    // Parse the priority fees.
    let priority_fees = fee_options.priority_fees.clone();
    // Parse the fee records.
    let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
    let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;

    // Pad the vectors to length `k`.
    let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
    let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);

    Ok(priority_fees.zip(fee_records).collect())
}

/// Additional options that are common across a number of commands.
#[derive(Parser, Clone, Debug, Default)]
pub struct ExtraOptions {
    #[clap(
        short,
        long,
        help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
        default_value = "false"
    )]
    pub(crate) yes: bool,
    #[clap(
        long,
        help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
    )]
    pub(crate) consensus_version: Option<u8>,
    #[clap(
        long,
        help = "Seconds to wait for a block to appear when searching for a transaction.",
        default_value = "8"
    )]
    pub(crate) max_wait: usize,
    #[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
    pub(crate) blocks_to_check: usize,
}

// A helper function to get the consensus version from the fee options.
// If a consensus version is not provided, then attempt to query the current block height and use it to determine the version.
pub fn get_consensus_version(
    consensus_version: &Option<u8>,
    endpoint: &str,
    network: NetworkName,
    heights: &[u32],
    context: &Context,
    network_retries: u32,
) -> Result<ConsensusVersion> {
    // Get the consensus version.
    let result = match consensus_version {
        Some(1) => Ok(ConsensusVersion::V1),
        Some(2) => Ok(ConsensusVersion::V2),
        Some(3) => Ok(ConsensusVersion::V3),
        Some(4) => Ok(ConsensusVersion::V4),
        Some(5) => Ok(ConsensusVersion::V5),
        Some(6) => Ok(ConsensusVersion::V6),
        Some(7) => Ok(ConsensusVersion::V7),
        Some(8) => Ok(ConsensusVersion::V8),
        Some(9) => Ok(ConsensusVersion::V9),
        Some(10) => Ok(ConsensusVersion::V10),
        Some(11) => Ok(ConsensusVersion::V11),
        Some(12) => Ok(ConsensusVersion::V12),
        Some(13) => Ok(ConsensusVersion::V13),
        Some(14) => Ok(ConsensusVersion::V14),
        Some(15) => Ok(ConsensusVersion::V15),
        Some(16) => Ok(ConsensusVersion::V16),
        Some(17) => Ok(ConsensusVersion::V17),
        Some(18) => Ok(ConsensusVersion::V18),
        // If none is provided, then attempt to query the current block height and use it to determine the version.
        None => {
            println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
            // Get the consensus heights for the current network.
            get_latest_block_height(endpoint, network, context, network_retries)
                .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
                .map_err(|_| {
                    crate::errors::custom(
                        "Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
                    )
                        .into()
                })
        }
        Some(version) => Err(crate::errors::custom(format!("Invalid consensus version: {version}")).into()),
    };

    // Check `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
    // If it returns a result and does not match the given version, print a warning.
    if let Ok(consensus_version) = result
        && let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network, network_retries)
    {
        println!("⚠️ Warning: {e}");
    }

    result
}

/// A helper function to check for a consensus version mismatch against the network.
pub fn check_consensus_version_mismatch(
    consensus_version: ConsensusVersion,
    endpoint: &str,
    network: NetworkName,
    network_retries: u32,
) -> anyhow::Result<()> {
    // Check the `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
    if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
        && let Ok(response) = response.parse::<u8>()
    {
        let consensus_version = consensus_version as u8;
        if response != consensus_version {
            bail!("Expected consensus version {consensus_version} but found {response} at {endpoint}",);
        }
    }
    Ok(())
}

// A helper function to get the consensus version based on the block height.
// Note. This custom implementation is necessary because we use `snarkVM` with the `test_heights` feature enabled, which does not reflect the actual consensus version heights.
pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
    // Find the consensus version based on the block height.
    let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
        // If a consensus version was found at this height, return it.
        Ok(index) => index,
        // If the specified height was not found, determine whether to return an appropriate version.
        Err(index) => {
            if index == 0 {
                return Err(crate::errors::custom("Expected consensus version 1 to exist at height 0.").into());
            } else {
                // Return the appropriate version belonging to the height *lower* than the sought height.
                index - 1
            }
        }
    };
    // Convert the index to a consensus version.
    number_to_consensus_version(index + 1)
}

// A helper to convert an index to a consensus version.
pub fn number_to_consensus_version(index: usize) -> Result<ConsensusVersion> {
    match index {
        1 => Ok(ConsensusVersion::V1),
        2 => Ok(ConsensusVersion::V2),
        3 => Ok(ConsensusVersion::V3),
        4 => Ok(ConsensusVersion::V4),
        5 => Ok(ConsensusVersion::V5),
        6 => Ok(ConsensusVersion::V6),
        7 => Ok(ConsensusVersion::V7),
        8 => Ok(ConsensusVersion::V8),
        9 => Ok(ConsensusVersion::V9),
        10 => Ok(ConsensusVersion::V10),
        11 => Ok(ConsensusVersion::V11),
        12 => Ok(ConsensusVersion::V12),
        13 => Ok(ConsensusVersion::V13),
        14 => Ok(ConsensusVersion::V14),
        15 => Ok(ConsensusVersion::V15),
        16 => Ok(ConsensusVersion::V16),
        17 => Ok(ConsensusVersion::V17),
        18 => Ok(ConsensusVersion::V18),
        _ => Err(crate::errors::custom(format!(
            "Invalid consensus version: {index}. You may need to update Leo to support this version."
        ))
        .into()),
    }
}

/// Get the consensus heights for the current network.
/// First check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
/// Otherwise, if `is_devnet` is true, then return the test consensus heights.
/// Otherwise, return the consensus heights for the given network.
pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
    // Check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
    if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
        if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
            return heights;
        } else {
            println!(
                "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
            );
        }
    }
    // If `is_devnet` is true, then return the test consensus heights.
    // Otherwise, return the consensus heights for the given network.
    if is_devnet {
        TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
    } else {
        match network_name {
            NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
            NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
            NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
        }
        .into_iter()
        .map(|(_, v)| v)
        .collect_vec()
    }
}

/// Validates a vector of heights as consensus heights.
pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
    // There must be exactly one height per consensus version.
    let expected = ConsensusVersion::latest() as usize;
    ensure!(
        heights.len() == expected,
        "expected exactly {expected} consensus heights (one per consensus version), but found {}",
        heights.len()
    );
    // Assert that the genesis height is 0.
    ensure!(heights[0] == 0, "Genesis height must be 0.");
    // Assert that the consensus heights are strictly increasing.
    for window in heights.windows(2) {
        if window[0] >= window[1] {
            bail!("Heights must be strictly increasing, but found: {window:?}");
        }
    }
    Ok(())
}

/// What to do with a transaction produced by the CLI.
#[derive(Args, Clone, Debug)]
pub struct TransactionAction {
    #[arg(long, help = "Print the transaction to stdout.")]
    pub print: bool,
    #[arg(long, help = "Broadcast the transaction to the network.")]
    pub broadcast: bool,
    #[arg(long, help = "Save the transaction to the provided directory.")]
    pub save: Option<String>,
}

/// Returns the endpoint to interact with the network.
/// If the `--endpoint` options is not provided, it will default to the environment variable.
pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
    match endpoint {
        Some(endpoint) => Ok(endpoint.clone()),
        None => {
            // Load the endpoint from the environment.
            std::env::var("ENDPOINT").map_err(|_| {
                crate::errors::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.")
                    .into()
            })
        }
    }
}

/// Returns the network name.
/// If the `--network` options is not provided, it will default to the environment variable.
pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
    match network {
        Some(network) => Ok(*network),
        None => {
            // Load the network from the environment.
            let network = std::env::var("NETWORK").map_err(|_| {
                crate::errors::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
            })?;
            // Parse the network.
            Ok(NetworkName::from_str(&network)?)
        }
    }
}

/// Returns the private key.
/// If the `--private-key` options is not provided, it will default to the environment variable.
pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
    match private_key {
        Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
        None => {
            // Load the private key from the environment.
            let private_key = std::env::var("PRIVATE_KEY").map_err(|e| {
                crate::errors::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}"))
            })?;
            // Parse the private key.
            Ok(PrivateKey::<N>::from_str(&private_key)?)
        }
    }
}

/// Returns whether the devnet flag is set.
/// If the `--devnet` flag is not set, check if the environment variable is set, otherwise default to `false`.
pub fn get_is_devnet(devnet: bool) -> bool {
    if devnet { true } else { std::env::var("DEVNET").is_ok() }
}

#[cfg(test)]
mod test {
    use snarkvm::prelude::ConsensusVersion;

    #[test]
    fn test_latest_consensus_version() {
        assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V18); // If this fails, update the test and any code that matches on `ConsensusVersion`.
    }

    #[test]
    fn test_validate_consensus_heights() {
        let n = ConsensusVersion::latest() as u32;
        // Exactly one height per version, genesis 0, strictly increasing: ok.
        let valid: Vec<u32> = (0..n).collect();
        assert!(super::validate_consensus_heights(&valid).is_ok());
        // Wrong count is a graceful error, not a panic.
        assert!(super::validate_consensus_heights(&(0..n - 1).collect::<Vec<_>>()).is_err());
        // Empty input is a graceful error, not an index-out-of-bounds panic.
        assert!(super::validate_consensus_heights(&[]).is_err());
    }
}