1use 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
37pub struct NetworkConfig {
40 pub name: String,
42 pub genesis: Genesis<penumbra_sdk_app::genesis::AppState>,
44 pub network_dir: PathBuf,
46 pub network_validators: Vec<NetworkValidator>,
49 pub validators: Vec<Validator>,
52 pub peer_address_template: Option<String>,
55 pub tendermint_timeout_commit: Option<tendermint::Timeout>,
58}
59
60impl NetworkConfig {
61 #[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 if let Some(address) = allocation_address {
96 tracing::info!(%address, "adding dynamic allocation to genesis");
97 allocations.extend(NetworkAllocation::simple(address));
98 }
99 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 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 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 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 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 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 pub(crate) fn make_genesis(
276 app_state: penumbra_sdk_app::genesis::Content,
277 ) -> anyhow::Result<Genesis<penumbra_sdk_app::genesis::AppState>> {
278 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 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 max_bytes: MAX_BLOCK_TXS_PAYLOAD_BYTES as u64,
301 max_gas: -1,
304 time_iota_ms: 500,
306 },
307 evidence: tendermint::evidence::Params {
308 max_age_num_blocks: 130000,
311 max_age_duration: tendermint::evidence::Duration(Duration::from_secs(650000)),
313 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 app_hash: tendermint::AppHash::default(),
323 app_state: penumbra_sdk_app::genesis::AppState::Content(app_state),
324 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 pub fn write_configs(&self) -> anyhow::Result<()> {
347 for (n, v) in self.network_validators.iter().enumerate() {
349 let node_name = format!("node{n}");
351 let node_dir = self.network_dir.clone().join(node_name.clone());
352
353 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#[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#[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 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 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 pub fn simple(address: Address) -> Vec<Allocation> {
472 vec![
473 Allocation {
474 address: address.clone(),
475 raw_denom: "upenumbra".into(),
476 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#[derive(Debug, Deserialize, Clone)]
491pub struct TestnetFundingStream {
492 pub rate_bps: u16,
493 pub address: String,
494}
495
496#[derive(Deserialize)]
498pub struct NetworkValidator {
499 pub name: String,
500 pub website: String,
501 pub description: String,
502 pub funding_streams: Vec<TestnetFundingStream>,
503 pub sequence_number: u32,
505 pub external_address: Option<TendermintAddress>,
509 pub peer_address_template: Option<String>,
510 #[serde(default)]
511 pub keys: ValidatorKeys,
512}
513
514impl NetworkValidator {
515 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 pub fn from_reader(input: impl Read) -> Result<Vec<NetworkValidator>> {
523 Ok(serde_json::from_reader(input)?)
524 }
525 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 raw_amount: (25_000 * 10u128.pow(6)).into(),
540 raw_denom: delegation_denom.to_string(),
541 })
542 }
543 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 Some(a) => match a {
556 TendermintAddress::Tcp {
557 peer_id: _,
558 host,
559 port,
560 } => format!("{tm_node_id}@{}:{}", host, port).parse()?,
561 _ => {
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 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
606impl TryFrom<&NetworkValidator> for Validator {
609 type Error = anyhow::Error;
610 fn try_from(tv: &NetworkValidator) -> anyhow::Result<Validator> {
611 if tv.website.len() > 70 {
614 anyhow::bail!("validator website field must be less than 70 bytes");
615 }
616
617 if tv.name.len() > 140 {
619 anyhow::bail!("validator name must be less than 140 bytes");
620 }
621
622 if tv.description.len() > 280 {
624 anyhow::bail!("validator description must be less than 280 bytes");
625 }
626
627 Ok(Validator {
628 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 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 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 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 }