pd/network/
generate.rs

1//! Logic for creating a new testnet configuration.
2//! Used for deploying (approximately weekly) testnets
3//! for Penumbra.
4use crate::network::config::{get_network_dir, NetworkTendermintConfig, ValidatorKeys};
5use anyhow::{Context, Result};
6use penumbra_sdk_app::{
7    app::{MAX_BLOCK_TXS_PAYLOAD_BYTES, MAX_EVIDENCE_SIZE_BYTES},
8    params::AppParameters,
9};
10use penumbra_sdk_asset::{asset, STAKING_TOKEN_ASSET_ID};
11use penumbra_sdk_fee::genesis::Content as FeeContent;
12use penumbra_sdk_governance::genesis::Content as GovernanceContent;
13use penumbra_sdk_keys::{keys::SpendKey, Address};
14use penumbra_sdk_sct::genesis::Content as SctContent;
15use penumbra_sdk_sct::params::SctParameters;
16use penumbra_sdk_shielded_pool::{
17    genesis::{self as shielded_pool_genesis, Allocation, Content as ShieldedPoolContent},
18    params::ShieldedPoolParameters,
19};
20use penumbra_sdk_stake::{
21    genesis::Content as StakeContent, params::StakeParameters, validator::Validator,
22    DelegationToken, FundingStream, FundingStreams, GovernanceKey, IdentityKey,
23};
24use serde::{de, Deserialize};
25use std::{
26    fmt,
27    fs::File,
28    io::Read,
29    path::PathBuf,
30    str::FromStr,
31    time::{Duration, SystemTime, UNIX_EPOCH},
32};
33use tendermint::consensus::params::AbciParams;
34use tendermint::{node, public_key::Algorithm, Genesis, Time};
35use tendermint_config::net::Address as TendermintAddress;
36
37/// Represents a Penumbra network config, including initial validators
38/// and allocations at genesis time.
39pub struct NetworkConfig {
40    /// The name of the network
41    pub name: String,
42    /// The Tendermint genesis for initial chain state.
43    pub genesis: Genesis<penumbra_sdk_app::genesis::AppState>,
44    /// Path to local directory where config files will be written to
45    pub network_dir: PathBuf,
46    /// Set of validators at genesis. Uses the convenient wrapper type
47    /// to generate config files.
48    pub network_validators: Vec<NetworkValidator>,
49    /// Set of validators at genesis. This is the literal type embedded
50    /// inside configs, including the keys
51    pub validators: Vec<Validator>,
52    /// Hostname as string for a validator's p2p service. Will have
53    /// numbers affixed to it for each validator, e.g. "-0", "-1", etc.
54    pub peer_address_template: Option<String>,
55    /// The Tendermint `consensus.timeout_commit` value, controlling how long Tendermint should
56    /// wait after committing a block, before starting on the new height. If unspecified, `5s`.
57    pub tendermint_timeout_commit: Option<tendermint::Timeout>,
58}
59
60impl NetworkConfig {
61    /// Create a new testnet configuration, optionally customizing the allocations and validator
62    /// set. By default, will use the prepared Discord allocations and Penumbra Labs CI validator
63    /// configs.
64    #[allow(clippy::too_many_arguments)]
65    pub fn generate(
66        chain_id: &str,
67        network_dir: Option<PathBuf>,
68        peer_address_template: Option<String>,
69        external_addresses: Option<Vec<TendermintAddress>>,
70        allocations_input_file: Option<PathBuf>,
71        allocation_address: Option<Address>,
72        validators_input_file: Option<PathBuf>,
73        tendermint_timeout_commit: Option<tendermint::Timeout>,
74        active_validator_limit: Option<u64>,
75        epoch_duration: Option<u64>,
76        unbonding_delay: Option<u64>,
77        proposal_voting_blocks: Option<u64>,
78        gas_price_simple: Option<u64>,
79    ) -> anyhow::Result<NetworkConfig> {
80        let external_addresses = external_addresses.unwrap_or_default();
81
82        let network_validators = Self::collect_validators(
83            validators_input_file,
84            peer_address_template.clone(),
85            external_addresses,
86        )?;
87
88        let mut allocations = Self::collect_allocations(allocations_input_file)?;
89
90        for v in network_validators.iter() {
91            allocations.push(v.delegation_allocation()?);
92        }
93
94        // Add an extra allocation for a dynamic wallet address.
95        if let Some(address) = allocation_address {
96            tracing::info!(%address, "adding dynamic allocation to genesis");
97            allocations.extend(NetworkAllocation::simple(address));
98        }
99        // Convert to domain type, for use with other Penumbra interfaces.
100        // We do this conversion once and store it in the struct for convenience.
101        let validators: anyhow::Result<Vec<Validator>> =
102            network_validators.iter().map(|v| v.try_into()).collect();
103        let validators = validators?;
104
105        let app_state = Self::make_genesis_content(
106            chain_id,
107            allocations,
108            validators.to_vec(),
109            active_validator_limit,
110            epoch_duration,
111            unbonding_delay,
112            proposal_voting_blocks,
113            gas_price_simple,
114        )?;
115        let genesis = Self::make_genesis(app_state)?;
116
117        Ok(NetworkConfig {
118            name: chain_id.to_owned(),
119            genesis,
120            network_dir: get_network_dir(network_dir),
121            network_validators,
122            validators: validators.to_vec(),
123            peer_address_template,
124            tendermint_timeout_commit,
125        })
126    }
127
128    /// Prepare set of initial validators present at genesis. Optionally reads config values from a
129    /// JSON file, otherwise falls back to the Penumbra Labs CI validator configs used for
130    /// testnets.
131    fn collect_validators(
132        validators_input_file: Option<PathBuf>,
133        peer_address_template: Option<String>,
134        external_addresses: Vec<TendermintAddress>,
135    ) -> anyhow::Result<Vec<NetworkValidator>> {
136        let testnet_validators = if let Some(validators_input_file) = validators_input_file {
137            NetworkValidator::from_json(validators_input_file)?
138        } else {
139            static LATEST_VALIDATORS: &str = include_str!(env!("PD_LATEST_TESTNET_VALIDATORS"));
140            NetworkValidator::from_reader(std::io::Cursor::new(LATEST_VALIDATORS)).with_context(
141                || {
142                    format!(
143                        "could not parse default latest testnet validators file {:?}",
144                        env!("PD_LATEST_TESTNET_VALIDATORS")
145                    )
146                },
147            )?
148        };
149
150        if !external_addresses.is_empty() && external_addresses.len() != testnet_validators.len() {
151            anyhow::bail!("Number of validators did not equal number of external addresses");
152        }
153
154        Ok(testnet_validators
155            .into_iter()
156            .enumerate()
157            .map(|(i, v)| NetworkValidator {
158                peer_address_template: peer_address_template.as_ref().map(|t| format!("{t}-{i}")),
159                external_address: external_addresses.get(i).cloned(),
160                ..v
161            })
162            .collect())
163    }
164
165    /// Prepare a set of initial [Allocation]s present at genesis. Optionally reads allocation
166    /// files a CSV file, otherwise falls back to the historical requests of the testnet faucet
167    /// in the Penumbra Discord channel.
168    fn collect_allocations(
169        allocations_input_file: Option<PathBuf>,
170    ) -> anyhow::Result<Vec<Allocation>> {
171        if let Some(ref allocations_input_file) = allocations_input_file {
172            Ok(
173                NetworkAllocation::from_csv(allocations_input_file.to_path_buf()).with_context(
174                    || format!("could not parse allocations file {allocations_input_file:?}"),
175                )?,
176            )
177        } else {
178            // Default to latest testnet allocations computed in the build script.
179            static LATEST_ALLOCATIONS: &str = include_str!(env!("PD_LATEST_TESTNET_ALLOCATIONS"));
180            Ok(
181                NetworkAllocation::from_reader(std::io::Cursor::new(LATEST_ALLOCATIONS))
182                    .with_context(|| {
183                        format!(
184                            "could not parse default latest testnet allocations file {:?}",
185                            env!("PD_LATEST_TESTNET_ALLOCATIONS")
186                        )
187                    })?,
188            )
189        }
190    }
191
192    /// Create a full genesis configuration for inclusion in the tendermint
193    /// genesis config.
194    fn make_genesis_content(
195        chain_id: &str,
196        allocations: Vec<Allocation>,
197        validators: Vec<Validator>,
198        active_validator_limit: Option<u64>,
199        epoch_duration: Option<u64>,
200        unbonding_delay: Option<u64>,
201        proposal_voting_blocks: Option<u64>,
202        gas_price_simple: Option<u64>,
203    ) -> anyhow::Result<penumbra_sdk_app::genesis::Content> {
204        let default_gov_params = penumbra_sdk_governance::params::GovernanceParameters::default();
205
206        let gov_params = penumbra_sdk_governance::params::GovernanceParameters {
207            proposal_voting_blocks: proposal_voting_blocks
208                .unwrap_or(default_gov_params.proposal_voting_blocks),
209            ..default_gov_params
210        };
211
212        // Look up default app params, so we can fill in defaults.
213        let default_app_params = AppParameters::default();
214
215        let gas_price_simple = gas_price_simple.unwrap_or_default();
216
217        let app_state = penumbra_sdk_app::genesis::Content {
218            chain_id: chain_id.to_string(),
219            stake_content: StakeContent {
220                validators: validators.into_iter().map(Into::into).collect(),
221                stake_params: StakeParameters {
222                    active_validator_limit: active_validator_limit
223                        .unwrap_or(default_app_params.stake_params.active_validator_limit),
224                    unbonding_delay: unbonding_delay
225                        .unwrap_or(default_app_params.stake_params.unbonding_delay),
226                    ..Default::default()
227                },
228            },
229            fee_content: FeeContent {
230                fee_params: penumbra_sdk_fee::params::FeeParameters {
231                    fixed_gas_prices: penumbra_sdk_fee::GasPrices {
232                        block_space_price: gas_price_simple,
233                        compact_block_space_price: gas_price_simple,
234                        verification_price: gas_price_simple,
235                        execution_price: gas_price_simple,
236                        asset_id: *STAKING_TOKEN_ASSET_ID,
237                    },
238                    fixed_alt_gas_prices: vec![
239                        penumbra_sdk_fee::GasPrices {
240                            block_space_price: 10 * gas_price_simple,
241                            compact_block_space_price: 10 * gas_price_simple,
242                            verification_price: 10 * gas_price_simple,
243                            execution_price: 10 * gas_price_simple,
244                            asset_id: asset::REGISTRY.parse_unit("gm").id(),
245                        },
246                        penumbra_sdk_fee::GasPrices {
247                            block_space_price: 10 * gas_price_simple,
248                            compact_block_space_price: 10 * gas_price_simple,
249                            verification_price: 10 * gas_price_simple,
250                            execution_price: 10 * gas_price_simple,
251                            asset_id: asset::REGISTRY.parse_unit("gn").id(),
252                        },
253                    ],
254                },
255            },
256            governance_content: GovernanceContent {
257                governance_params: gov_params,
258            },
259            shielded_pool_content: ShieldedPoolContent {
260                shielded_pool_params: ShieldedPoolParameters::default(),
261                allocations: allocations.clone(),
262            },
263            sct_content: SctContent {
264                sct_params: SctParameters {
265                    epoch_duration: epoch_duration
266                        .unwrap_or(default_app_params.sct_params.epoch_duration),
267                },
268            },
269            ..Default::default()
270        };
271        Ok(app_state)
272    }
273
274    /// Build Tendermint genesis data, based on Penumbra initial application state.
275    pub(crate) fn make_genesis(
276        app_state: penumbra_sdk_app::genesis::Content,
277    ) -> anyhow::Result<Genesis<penumbra_sdk_app::genesis::AppState>> {
278        // Use now as genesis time
279        let genesis_time = Time::from_unix_timestamp(
280            SystemTime::now()
281                .duration_since(UNIX_EPOCH)
282                .context("expected that time travels linearly in a forward direction")?
283                .as_secs() as i64,
284            0,
285        )
286        .context("failed to convert current time into Time")?;
287
288        // Create Tendermint genesis data shared by all nodes
289        let genesis = Genesis {
290            genesis_time,
291            chain_id: app_state
292                .chain_id
293                .parse::<tendermint::chain::Id>()
294                .context("failed to parse chain ID")?,
295            initial_height: 0,
296            consensus_params: tendermint::consensus::Params {
297                abci: AbciParams::default(),
298                block: tendermint::block::Size {
299                    // 1MB
300                    max_bytes: MAX_BLOCK_TXS_PAYLOAD_BYTES as u64,
301                    // Set to infinity since a chain running Penumbra won't use
302                    // cometbft's notion of gas.
303                    max_gas: -1,
304                    // Minimum time increment between consecutive blocks.
305                    time_iota_ms: 500,
306                },
307                evidence: tendermint::evidence::Params {
308                    // We should keep this in approximate sync with the recommended default for
309                    // `StakeParameters::unbonding_delay`, this is roughly a week.
310                    max_age_num_blocks: 130000,
311                    // Similarly, we set the max age duration for evidence to be a little over a week.
312                    max_age_duration: tendermint::evidence::Duration(Duration::from_secs(650000)),
313                    // 30KB
314                    max_bytes: MAX_EVIDENCE_SIZE_BYTES as i64,
315                },
316                validator: tendermint::consensus::params::ValidatorParams {
317                    pub_key_types: vec![Algorithm::Ed25519],
318                },
319                version: Some(tendermint::consensus::params::VersionParams { app: 0 }),
320            },
321            // always empty in genesis json
322            app_hash: tendermint::AppHash::default(),
323            app_state: penumbra_sdk_app::genesis::AppState::Content(app_state),
324            // Set empty validator set for Tendermint config, which falls back to reading
325            // validators from the AppState, via ResponseInitChain:
326            // https://docs.tendermint.com/v0.32/tendermint-core/using-tendermint.html
327            validators: vec![],
328        };
329        Ok(genesis)
330    }
331
332    pub(crate) fn make_checkpoint(
333        genesis: Genesis<penumbra_sdk_app::genesis::AppState>,
334        checkpoint: Option<Vec<u8>>,
335    ) -> Genesis<penumbra_sdk_app::genesis::AppState> {
336        match checkpoint {
337            Some(checkpoint) => Genesis {
338                app_state: penumbra_sdk_app::genesis::AppState::Checkpoint(checkpoint),
339                ..genesis
340            },
341            None => genesis,
342        }
343    }
344
345    /// Generate and write to disk the Tendermint configs for each validator at genesis.
346    pub fn write_configs(&self) -> anyhow::Result<()> {
347        // Loop over each validator and write its config separately.
348        for (n, v) in self.network_validators.iter().enumerate() {
349            // Create the directory for this node
350            let node_name = format!("node{n}");
351            let node_dir = self.network_dir.clone().join(node_name.clone());
352
353            // Each node should include only the IPs for *other* nodes in their peers list.
354            let ips_minus_mine: anyhow::Result<Vec<TendermintAddress>> = self
355                .network_validators
356                .iter()
357                .map(|v| v.peering_address())
358                .filter(|a| {
359                    *a.as_ref().expect("able to get address ref")
360                        != v.peering_address()
361                            .expect("able to get peering address ref")
362                })
363                .collect();
364            let ips_minus_mine = ips_minus_mine?;
365            tracing::debug!(?ips_minus_mine, "Found these peer ips");
366
367            let external_address: Option<TendermintAddress> = v.external_address.as_ref().cloned();
368            let mut tm_config = NetworkTendermintConfig::new(
369                &node_name,
370                ips_minus_mine,
371                external_address,
372                None,
373                None,
374            )?;
375            if let Some(timeout_commit) = self.tendermint_timeout_commit {
376                tm_config.0.consensus.timeout_commit = timeout_commit;
377            }
378            tm_config.write_config(node_dir, v, &self.genesis)?;
379        }
380        Ok(())
381    }
382}
383
384/// Create a new testnet definition, including genesis and at least one
385/// validator config. Write all configs to the target testnet dir,
386/// defaulting to `~/.penumbra/<chain_id>`.
387#[allow(clippy::too_many_arguments)]
388pub fn network_generate(
389    network_dir: Option<PathBuf>,
390    chain_id: &str,
391    active_validator_limit: Option<u64>,
392    tendermint_timeout_commit: Option<tendermint::Timeout>,
393    epoch_duration: Option<u64>,
394    unbonding_delay: Option<u64>,
395    peer_address_template: Option<String>,
396    external_addresses: Vec<TendermintAddress>,
397    validators_input_file: Option<PathBuf>,
398    allocations_input_file: Option<PathBuf>,
399    allocation_address: Option<Address>,
400    proposal_voting_blocks: Option<u64>,
401    gas_price_simple: Option<u64>,
402) -> anyhow::Result<()> {
403    tracing::info!(?chain_id, "Generating network config");
404    let t = NetworkConfig::generate(
405        chain_id,
406        network_dir,
407        peer_address_template,
408        Some(external_addresses),
409        allocations_input_file,
410        allocation_address,
411        validators_input_file,
412        tendermint_timeout_commit,
413        active_validator_limit,
414        epoch_duration,
415        unbonding_delay,
416        proposal_voting_blocks,
417        gas_price_simple,
418    )?;
419    tracing::info!(
420        n_validators = t.validators.len(),
421        chain_id = %t.genesis.chain_id,
422        "Writing config files for network"
423    );
424    t.write_configs()?;
425    Ok(())
426}
427
428/// Represents initial allocations to the testnet.
429#[derive(Debug, Deserialize)]
430pub struct NetworkAllocation {
431    #[serde(deserialize_with = "string_u128")]
432    pub amount: u128,
433    pub denom: String,
434    pub address: String,
435}
436
437impl NetworkAllocation {
438    /// Import allocations from a CSV file. The format is simple:
439    ///
440    ///   amount,denom,address
441    ///
442    /// Typically these CSV files are generated by Galileo.
443    pub fn from_csv(csv_filepath: PathBuf) -> Result<Vec<Allocation>> {
444        let allocations_file = File::open(&csv_filepath)
445            .with_context(|| format!("cannot open file {csv_filepath:?}"))?;
446        Self::from_reader(allocations_file)
447    }
448    /// Import allocations from a reader object that emits CSV.
449    pub fn from_reader(csv_input: impl Read) -> Result<Vec<Allocation>> {
450        let mut rdr = csv::Reader::from_reader(csv_input);
451        let mut res = vec![];
452        for (line, result) in rdr.deserialize().enumerate() {
453            let record: NetworkAllocation = result?;
454            let record: shielded_pool_genesis::Allocation =
455                record.try_into().with_context(|| {
456                    format!("invalid allocation in entry {line} of allocations file")
457                })?;
458            res.push(record);
459        }
460
461        if res.is_empty() {
462            anyhow::bail!("parsed no entries from allocations input file; is the file valid CSV?");
463        }
464
465        Ok(res)
466    }
467    /// Creates a basic set of genesis [Allocation]s for the provided [Address].
468    /// Returns multiple Allocations, so that it's immediately possible to use the DEX,
469    /// for basic interactive testing of swap behavior.
470    /// For more control over precise allocation amounts, use [from_csv].
471    pub fn simple(address: Address) -> Vec<Allocation> {
472        vec![
473            Allocation {
474                address: address.clone(),
475                raw_denom: "upenumbra".into(),
476                // The `upenumbra` base denom is millionths, so `10^6 * n`
477                // results in `n` `penumbra` tokens.
478                raw_amount: (100_000 * 10u128.pow(6)).into(),
479            },
480            Allocation {
481                address: address.clone(),
482                raw_denom: "test_usd".into(),
483                raw_amount: (1_000 as u128).into(),
484            },
485        ]
486    }
487}
488
489/// Represents a funding stream within a testnet configuration file.
490#[derive(Debug, Deserialize, Clone)]
491pub struct TestnetFundingStream {
492    pub rate_bps: u16,
493    pub address: String,
494}
495
496/// Represents testnet validators in configuration files.
497#[derive(Deserialize)]
498pub struct NetworkValidator {
499    pub name: String,
500    pub website: String,
501    pub description: String,
502    pub funding_streams: Vec<TestnetFundingStream>,
503    /// All validator identities
504    pub sequence_number: u32,
505    /// Optional `external_address` field for Tendermint config.
506    /// Instructs peers to connect to this node's P2P service
507    /// on this address.
508    pub external_address: Option<TendermintAddress>,
509    pub peer_address_template: Option<String>,
510    #[serde(default)]
511    pub keys: ValidatorKeys,
512}
513
514impl NetworkValidator {
515    /// Import validator configs from a JSON file.
516    pub fn from_json(json_filepath: PathBuf) -> Result<Vec<NetworkValidator>> {
517        let validators_file = File::open(&json_filepath)
518            .with_context(|| format!("cannot open file {json_filepath:?}"))?;
519        Self::from_reader(validators_file)
520    }
521    /// Import validator configs from a reader object that emits JSON.
522    pub fn from_reader(input: impl Read) -> Result<Vec<NetworkValidator>> {
523        Ok(serde_json::from_reader(input)?)
524    }
525    /// Generate iniital delegation allocation for inclusion in genesis.
526    pub fn delegation_allocation(&self) -> Result<Allocation> {
527        let spend_key = SpendKey::from(self.keys.validator_spend_key.clone());
528        let fvk = spend_key.full_viewing_key();
529        let ivk = fvk.incoming();
530        let (dest, _dtk_d) = ivk.payment_address(0u32.into());
531
532        let identity_key: IdentityKey = IdentityKey(fvk.spend_verification_key().clone().into());
533        let delegation_denom = DelegationToken::from(&identity_key).denom();
534        Ok(Allocation {
535            address: dest,
536            // Add an initial allocation of 25,000 delegation tokens,
537            // starting them with 2.5x the individual allocations to discord users.
538            // 25,000 delegation tokens * 1e6 udelegation factor
539            raw_amount: (25_000 * 10u128.pow(6)).into(),
540            raw_denom: delegation_denom.to_string(),
541        })
542    }
543    /// Return a URL for Tendermint P2P service for this node.
544    ///
545    /// In order for the set of genesis validators to communicate with each other,
546    /// they must have initial peer information seeded into their Tendermint config files.
547    /// If an `external_address` was set, use that. Next, check for a `peer_address_template`.
548    /// Finally, fall back to localhost.
549    pub fn peering_address(&self) -> anyhow::Result<TendermintAddress> {
550        let tm_node_id = node::Id::from(self.keys.node_key_pk.ed25519().expect("ed25519 key"));
551        tracing::debug!(?self.name, ?self.external_address, ?self.peer_address_template, "Looking up peering_address");
552        let r: TendermintAddress = match &self.external_address {
553            // The `external_address` is a TendermintAddress, so unpack as enum to retrieve
554            // the host/port info.
555            Some(a) => match a {
556                TendermintAddress::Tcp {
557                    peer_id: _,
558                    host,
559                    port,
560                } => format!("{tm_node_id}@{}:{}", host, port).parse()?,
561                // The other enum type is TendermintAddress::Unix, see
562                // https://docs.rs/tendermint-config/0.33.0/tendermint_config/index.html
563                _ => {
564                    anyhow::bail!(
565                        "Only TCP format is supported for tendermint addresses: {}",
566                        a
567                    );
568                }
569            },
570            None => match &self.peer_address_template {
571                Some(t) => format!("{tm_node_id}@{t}:26656").parse()?,
572                None => format!("{tm_node_id}@127.0.0.1:26656").parse()?,
573            },
574        };
575        Ok(r)
576    }
577
578    /// Hardcoded initial state for Tendermint, used for writing configs.
579    // Easiest to hardcode since we never change these.
580    pub fn initial_state() -> String {
581        r#"{
582        "height": "0",
583        "round": 0,
584        "step": 0
585    }
586    "#
587        .to_string()
588    }
589}
590
591impl Default for NetworkValidator {
592    fn default() -> Self {
593        Self {
594            name: "".to_string(),
595            website: "".to_string(),
596            description: "".to_string(),
597            funding_streams: Vec::<TestnetFundingStream>::new(),
598            sequence_number: 0,
599            external_address: None,
600            peer_address_template: None,
601            keys: ValidatorKeys::generate(),
602        }
603    }
604}
605
606// The core Penumbra logic deals with `Validator`s, to make sure our convenient
607// wrapper type can resolve as a `Validator` when needed.
608impl TryFrom<&NetworkValidator> for Validator {
609    type Error = anyhow::Error;
610    fn try_from(tv: &NetworkValidator) -> anyhow::Result<Validator> {
611        // Validation:
612        // - Website has a max length of 70 bytes
613        if tv.website.len() > 70 {
614            anyhow::bail!("validator website field must be less than 70 bytes");
615        }
616
617        // - Name has a max length of 140 bytes
618        if tv.name.len() > 140 {
619            anyhow::bail!("validator name must be less than 140 bytes");
620        }
621
622        // - Description has a max length of 280 bytes
623        if tv.description.len() > 280 {
624            anyhow::bail!("validator description must be less than 280 bytes");
625        }
626
627        Ok(Validator {
628            // Currently there's no way to set validator keys beyond
629            // manually editing the genesis.json. Otherwise they
630            // will be randomly generated keys.
631            identity_key: IdentityKey(tv.keys.validator_id_vk.into()),
632            governance_key: GovernanceKey(tv.keys.validator_id_vk),
633            consensus_key: tv.keys.validator_cons_pk,
634            name: tv.name.clone(),
635            website: tv.website.clone(),
636            description: tv.description.clone(),
637            enabled: true,
638            funding_streams: FundingStreams::try_from(
639                tv.funding_streams
640                    .iter()
641                    .map(|fs| {
642                        Ok(FundingStream::ToAddress {
643                            address: Address::from_str(&fs.address)
644                                .context("invalid funding stream address in validators.json")?,
645                            rate_bps: fs.rate_bps,
646                        })
647                    })
648                    .collect::<Result<Vec<FundingStream>, anyhow::Error>>()?,
649            )
650            .context("unable to construct funding streams from validators.json")?,
651            sequence_number: tv.sequence_number,
652        })
653    }
654}
655
656impl TryFrom<NetworkAllocation> for shielded_pool_genesis::Allocation {
657    type Error = anyhow::Error;
658
659    fn try_from(a: NetworkAllocation) -> anyhow::Result<shielded_pool_genesis::Allocation> {
660        Ok(shielded_pool_genesis::Allocation {
661            raw_amount: a.amount.into(),
662            raw_denom: a.denom.clone(),
663            address: Address::from_str(&a.address).with_context(|| {
664                format!(
665                    "invalid address format in genesis allocations: {}",
666                    &a.address
667                )
668            })?,
669        })
670    }
671}
672
673fn string_u128<'de, D>(deserializer: D) -> Result<u128, D::Error>
674where
675    D: de::Deserializer<'de>,
676{
677    struct U128StringVisitor;
678
679    impl<'de> de::Visitor<'de> for U128StringVisitor {
680        type Value = u128;
681
682        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
683            formatter.write_str("a string containing a u128 with optional underscores")
684        }
685
686        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
687        where
688            E: de::Error,
689        {
690            let r = v.replace('_', "");
691            r.parse::<u128>().map_err(E::custom)
692        }
693
694        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
695        where
696            E: de::Error,
697        {
698            Ok(v as u128)
699        }
700
701        fn visit_u128<E>(self, v: u128) -> std::prelude::v1::Result<Self::Value, E>
702        where
703            E: de::Error,
704        {
705            Ok(v)
706        }
707    }
708
709    deserializer.deserialize_any(U128StringVisitor)
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn parse_allocations_from_good_csv() -> anyhow::Result<()> {
718        let csv_content = r#"
719"amount","denom","address"
720"100000","udelegation_penumbravalid1jzcc6vsm29am9ggs8z0d7s9jk9uf8tfrqg7hglc9ufs7r23nu5yqtw77ex","penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk"
721"100000","upenumbra","penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk"
722"100000","udelegation_penumbravalid1p2hfuch2p8rshyc90qa23gqk82s74tqcu3x2x3y5tfwpzth4vvrq2gv283","penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg"
723"100000","upenumbra","penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg"
724"100000","udelegation_penumbravalid182k8x46hg5vx3ez8ec58ze5yd6a3q4q3fkx45ddt5jahnzz0xyyqdtz7hc","penumbra100zd92fg6x27wc0mlu48cd6phq420u7ep59kzdalg2cq66mjkyl0xr54z0c64gectnj44mv5k2vyjjsz5gyd5gq33a6wnqzvgu2fz7namz7usazsl6p8wza83gcpwt8q76rc4y"
725"100000","upenumbra","penumbra100zd92fg6x27wc0mlu48cd6phq420u7ep59kzdalg2cq66mjkyl0xr54z0c64gectnj44mv5k2vyjjsz5gyd5gq33a6wnqzvgu2fz7namz7usazsl6p8wza83gcpwt8q76rc4y"
726"100000","udelegation_penumbravalid1t2hr2lj5n2jt3hftzjw3strjllnakc7jtw234d229x3zakhaqsqsg9yarw","penumbra1xap8sgefy9rl2nfvsse0h4y6c25hy2n20ymr5w7hs28m9xemt3tmz88atyulswumc32sv7h937wnfhyct282de66zm75nk6ywq3d4r32p5ju0cnscj2rraesnrxr9lvk6hcazp"
727"100000","upenumbra","penumbra1xap8sgefy9rl2nfvsse0h4y6c25hy2n20ymr5w7hs28m9xemt3tmz88atyulswumc32sv7h937wnfhyct282de66zm75nk6ywq3d4r32p5ju0cnscj2rraesnrxr9lvk6hcazp"
728"#;
729        let allos = NetworkAllocation::from_reader(csv_content.as_bytes())?;
730
731        let a1 = &allos[0];
732        assert!(a1.raw_denom == "udelegation_penumbravalid1jzcc6vsm29am9ggs8z0d7s9jk9uf8tfrqg7hglc9ufs7r23nu5yqtw77ex");
733        assert!(a1.address == Address::from_str("penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk")?);
734        assert!(a1.raw_amount.value() == 100000);
735
736        let a2 = &allos[1];
737        assert!(a2.raw_denom == "upenumbra");
738        assert!(a2.address == Address::from_str("penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk")?);
739        assert!(a2.raw_amount.value() == 100000);
740
741        Ok(())
742    }
743
744    #[test]
745    fn parse_allocations_from_bad_csv() -> anyhow::Result<()> {
746        let csv_content = r#"
747"amount","denom","address"\n"100000","udelegation_penumbravalid1jzcc6vsm29am9ggs8z0d7s9jk9uf8tfrqg7hglc9ufs7r23nu5yqtw77ex","penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk"\n"100000","upenumbra","penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk"\n"100000","udelegation_penumbravalid1p2hfuch2p8rshyc90qa23gqk82s74tqcu3x2x3y5tfwpzth4vvrq2gv283","penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg"\n"100000","upenumbra","penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg"\n"100000","udelegation_penumbravalid182k8x46hg5vx3ez8ec58ze5yd6a3q4q3fkx45ddt5jahnzz0xyyqdtz7hc","penumbra100zd92fg6x27wc0mlu48cd6phq420u7ep59kzdalg2cq66mjkyl0xr54z0c64gectnj44mv5k2vyjjsz5gyd5gq33a6wnqzvgu2fz7namz7usazsl6p8wza83gcpwt8q76rc4y"\n"100000","upenumbra","penumbra100zd92fg6x27wc0mlu48cd6phq420u7ep59kzdalg2cq66mjkyl0xr54z0c64gectnj44mv5k2vyjjsz5gyd5gq33a6wnqzvgu2fz7namz7usazsl6p8wza83gcpwt8q76rc4y"\n"100000","udelegation_penumbravalid1t2hr2lj5n2jt3hftzjw3strjllnakc7jtw234d229x3zakhaqsqsg9yarw","penumbra1xap8sgefy9rl2nfvsse0h4y6c25hy2n20ymr5w7hs28m9xemt3tmz88atyulswumc32sv7h937wnfhyct282de66zm75nk6ywq3d4r32p5ju0cnscj2rraesnrxr9lvk6hcazp"\n"100000","upenumbra","penumbra1xap8sgefy9rl2nfvsse0h4y6c25hy2n20ymr5w7hs28m9xemt3tmz88atyulswumc32sv7h937wnfhyct282de66zm75nk6ywq3d4r32p5ju0cnscj2rraesnrxr9lvk6hcazp"\n
748"#;
749        let result = NetworkAllocation::from_reader(csv_content.as_bytes());
750        assert!(result.is_err());
751        Ok(())
752    }
753
754    #[test]
755    /// Generate a config suitable for local testing: no custom address information, no additional
756    /// validators at genesis.
757    fn generate_devnet_config() -> anyhow::Result<()> {
758        let testnet_config = NetworkConfig::generate(
759            "test-chain-1234",
760            None,
761            None,
762            None,
763            None,
764            None,
765            None,
766            None,
767            None,
768            None,
769            None,
770            None,
771            None,
772        )?;
773        assert_eq!(testnet_config.name, "test-chain-1234");
774        assert_eq!(testnet_config.genesis.validators.len(), 0);
775        // No external address template was given, so only 1 validator will be present.
776        let penumbra_sdk_app::genesis::AppState::Content(app_state) =
777            testnet_config.genesis.app_state
778        else {
779            unimplemented!("TODO: support checkpointed app state")
780        };
781        assert_eq!(app_state.stake_content.validators.len(), 1);
782        Ok(())
783    }
784
785    #[test]
786    /// Generate a config suitable for a public testnet: custom validators input file,
787    /// increasing the default validators from 1 -> 2.
788    fn generate_network_config() -> anyhow::Result<()> {
789        let ci_validators_filepath = PathBuf::from("../../../testnets/validators-ci.json");
790        let testnet_config = NetworkConfig::generate(
791            "test-chain-4567",
792            None,
793            Some(String::from("validator.local")),
794            None,
795            None,
796            None,
797            Some(ci_validators_filepath),
798            None,
799            None,
800            None,
801            None,
802            None,
803            None,
804        )?;
805        assert_eq!(testnet_config.name, "test-chain-4567");
806        assert_eq!(testnet_config.genesis.validators.len(), 0);
807        let penumbra_sdk_app::genesis::AppState::Content(app_state) =
808            testnet_config.genesis.app_state
809        else {
810            unimplemented!("TODO: support checkpointed app state")
811        };
812        assert_eq!(app_state.stake_content.validators.len(), 2);
813        Ok(())
814    }
815
816    //    #[test]
817    //    fn testnet_validator_to_validator_conversion() -> anyhow::Result<()> {
818    //        let tv = NetworkValidator::default();
819    //        let v: Validator = tv.try_into()?;
820    //        assert!(v.website == "");
821    //        Ok(())
822    //    }
823}