1use crate::genesis_validate::validate_genesis;
7use anyhow::Context;
8use chrono::{DateTime, Utc};
9use num_rational::Rational32;
10use serde::de::{self, DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
11use serde::{Deserialize, Deserializer, Serialize};
12use serde_json::Serializer;
13use sha2::digest::Digest;
14use smart_default::SmartDefault;
15use std::collections::HashSet;
16use std::fmt;
17use std::fs::File;
18use std::io::{BufReader, Read};
19use std::path::{Path, PathBuf};
20use tracing::warn;
21use unc_config_utils::ValidationError;
22use unc_parameters::{RuntimeConfig, RuntimeConfigView};
23use unc_primitives::epoch_manager::EpochConfig;
24use unc_primitives::shard_layout::ShardLayout;
25use unc_primitives::types::validator_power_and_pledge::ValidatorPowerAndPledge;
26use unc_primitives::types::StateRoot;
27use unc_primitives::{
28 hash::CryptoHash,
29 serialize::dec_format,
30 state_record::StateRecord,
31 types::{
32 AccountId, AccountInfo, Balance, BlockHeight, BlockHeightDelta, Gas, NumBlocks, NumSeats,
33 },
34 version::ProtocolVersion,
35};
36
37const MAX_GAS_PRICE: Balance = 10_000_000_000_000_000_000_000;
38
39fn default_online_min_threshold() -> Rational32 {
40 Rational32::new(90, 100)
41}
42
43fn default_online_max_threshold() -> Rational32 {
44 Rational32::new(99, 100)
45}
46
47fn default_minimum_pledge_divisor() -> u64 {
48 10
49}
50
51fn default_protocol_upgrade_pledge_threshold() -> Rational32 {
52 Rational32::new(8, 10)
53}
54
55fn default_shard_layout() -> ShardLayout {
56 ShardLayout::v0_single_shard()
57}
58
59fn default_minimum_pledge_ratio() -> Rational32 {
60 Rational32::new(160, 1_000_000)
61}
62
63fn default_minimum_validators_per_shard() -> u64 {
64 1
65}
66
67fn default_num_chunk_only_producer_seats() -> u64 {
68 300
69}
70
71fn default_use_production_config() -> bool {
72 false
73}
74
75fn default_max_kickout_pledge_threshold() -> u8 {
76 100
77}
78
79#[derive(Debug, Clone, SmartDefault, serde::Serialize, serde::Deserialize)]
80pub struct GenesisConfig {
81 pub protocol_version: ProtocolVersion,
83 #[default(Utc::now())]
85 pub genesis_time: DateTime<Utc>,
86 pub chain_id: String,
89 pub genesis_height: BlockHeight,
91 pub num_block_producer_seats: NumSeats,
93 pub num_block_producer_seats_per_shard: Vec<NumSeats>,
97 pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
99 #[serde(default = "default_protocol_upgrade_pledge_threshold")]
101 #[default(Rational32::new(8, 10))]
102 pub protocol_upgrade_pledge_threshold: Rational32,
103 pub epoch_length: BlockHeightDelta,
105 pub gas_limit: Gas,
107 #[serde(with = "dec_format")]
109 pub min_gas_price: Balance,
110 #[serde(with = "dec_format")]
111 #[default(MAX_GAS_PRICE)]
112 pub max_gas_price: Balance,
113 pub block_producer_kickout_threshold: u8,
115 pub chunk_producer_kickout_threshold: u8,
117 #[serde(default = "default_online_min_threshold")]
119 #[default(Rational32::new(90, 100))]
120 pub online_min_threshold: Rational32,
121 #[serde(default = "default_online_max_threshold")]
123 #[default(Rational32::new(99, 100))]
124 pub online_max_threshold: Rational32,
125 #[default(Rational32::from_integer(0))]
127 pub gas_price_adjustment_rate: Rational32,
128 pub validators: Vec<AccountInfo>,
130 pub transaction_validity_period: NumBlocks,
132 #[default(Rational32::from_integer(0))]
134 pub protocol_reward_rate: Rational32,
135 #[default(Rational32::from_integer(0))]
137 pub max_inflation_rate: Rational32,
138 #[serde(with = "dec_format")]
140 pub total_supply: Balance,
141 #[default(31536000)]
143 pub num_blocks_per_year: NumBlocks,
144 #[default("unc".parse().unwrap())]
146 pub protocol_treasury_account: AccountId,
147 #[serde(with = "dec_format")]
149 pub fishermen_threshold: Balance,
150 #[serde(default = "default_minimum_pledge_divisor")]
152 #[default(10)]
153 pub minimum_pledge_divisor: u64,
154 #[serde(default = "default_shard_layout")]
156 #[default(ShardLayout::v0_single_shard())]
157 pub shard_layout: ShardLayout,
158 #[serde(default = "default_num_chunk_only_producer_seats")]
159 #[default(300)]
160 pub num_chunk_only_producer_seats: NumSeats,
161 #[serde(default = "default_minimum_validators_per_shard")]
163 #[default(1)]
164 pub minimum_validators_per_shard: NumSeats,
165 #[serde(default = "default_max_kickout_pledge_threshold")]
166 #[default(100)]
167 pub max_kickout_pledge_perc: u8,
169 #[serde(default = "default_minimum_pledge_ratio")]
171 #[default(Rational32::new(160, 1_000_000))]
172 pub minimum_pledge_ratio: Rational32,
173 #[serde(default = "default_use_production_config")]
174 #[default(false)]
175 pub use_production_config: bool,
179}
180
181impl GenesisConfig {
182 pub fn use_production_config(&self) -> bool {
183 self.use_production_config
184 || self.chain_id == unc_primitives::chains::TESTNET
185 || self.chain_id == unc_primitives::chains::MAINNET
186 }
187}
188
189impl From<&GenesisConfig> for EpochConfig {
190 fn from(config: &GenesisConfig) -> Self {
191 EpochConfig {
192 epoch_length: config.epoch_length,
193 num_block_producer_seats: config.num_block_producer_seats,
194 num_block_producer_seats_per_shard: config.num_block_producer_seats_per_shard.clone(),
195 avg_hidden_validator_seats_per_shard: config
196 .avg_hidden_validator_seats_per_shard
197 .clone(),
198 block_producer_kickout_threshold: config.block_producer_kickout_threshold,
199 chunk_producer_kickout_threshold: config.chunk_producer_kickout_threshold,
200 fishermen_threshold: config.fishermen_threshold,
201 online_min_threshold: config.online_min_threshold,
202 online_max_threshold: config.online_max_threshold,
203 protocol_upgrade_pledge_threshold: config.protocol_upgrade_pledge_threshold,
204 minimum_pledge_divisor: config.minimum_pledge_divisor,
205 shard_layout: ShardLayout::v0_single_shard(),
206 validator_selection_config: unc_primitives::epoch_manager::ValidatorSelectionConfig {
207 num_chunk_only_producer_seats: config.num_chunk_only_producer_seats,
208 minimum_validators_per_shard: config.minimum_validators_per_shard,
209 minimum_pledge_ratio: config.minimum_pledge_ratio,
210 },
211 validator_max_kickout_pledge_perc: config.max_kickout_pledge_perc,
212 }
213 }
214}
215
216#[derive(
218 Debug,
219 Clone,
220 SmartDefault,
221 derive_more::AsRef,
222 derive_more::AsMut,
223 derive_more::From,
224 serde::Serialize,
225 serde::Deserialize,
226)]
227pub struct GenesisRecords(pub Vec<StateRecord>);
228
229fn no_value_and_null_as_default<'de, D, T>(de: D) -> Result<T, D::Error>
234where
235 D: Deserializer<'de>,
236 T: Deserialize<'de> + Default,
237{
238 let opt = Option::<T>::deserialize(de)?;
239 match opt {
240 None => Ok(T::default()),
241 Some(value) => Ok(value),
242 }
243}
244
245#[derive(Debug, Clone, SmartDefault, serde::Serialize, serde::Deserialize)]
246#[serde(untagged)]
247pub enum GenesisContents {
248 #[default]
250 Records { records: GenesisRecords },
251 RecordsFile { records_file: PathBuf },
256 StateRoots { state_roots: Vec<StateRoot> },
263}
264
265fn contents_are_from_record_file(contents: &GenesisContents) -> bool {
266 match contents {
267 GenesisContents::RecordsFile { .. } => true,
268 _ => false,
269 }
270}
271
272#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
277pub struct Genesis {
278 #[serde(flatten)]
279 pub config: GenesisConfig,
280 #[serde(
284 flatten,
285 deserialize_with = "no_value_and_null_as_default",
286 skip_serializing_if = "contents_are_from_record_file"
287 )]
288 pub contents: GenesisContents,
289}
290
291impl GenesisConfig {
292 pub fn from_json(value: &str) -> Self {
296 let json_str_without_comments: String =
297 unc_config_utils::strip_comments_from_json_str(&value.to_string())
298 .expect("Failed to strip comments from genesis config.");
299 serde_json::from_str(&json_str_without_comments)
300 .expect("Failed to deserialize the genesis config.")
301 }
302
303 pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
308 let mut file = File::open(path).with_context(|| "Could not open genesis config file.")?;
309 let mut json_str = String::new();
310 file.read_to_string(&mut json_str)?;
311 let json_str_without_comments: String =
312 unc_config_utils::strip_comments_from_json_str(&json_str)?;
313 let genesis_config: GenesisConfig = serde_json::from_str(&json_str_without_comments)
314 .with_context(|| "Failed to deserialize the genesis config.")?;
315 Ok(genesis_config)
316 }
317
318 pub fn to_file<P: AsRef<Path>>(&self, path: P) {
320 std::fs::write(
321 path,
322 serde_json::to_vec_pretty(self).expect("Error serializing the genesis config."),
323 )
324 .expect("Failed to create / write a genesis config file.");
325 }
326
327 pub fn validators(&self) -> Vec<ValidatorPowerAndPledge> {
329 self.validators
330 .iter()
331 .map(|account_info| {
332 ValidatorPowerAndPledge::new(
333 account_info.account_id.clone(),
334 account_info.public_key.clone(),
335 account_info.power,
336 account_info.pledging,
337 )
338 })
339 .collect()
340 }
341}
342
343impl GenesisRecords {
344 pub fn from_json(value: &str) -> Self {
348 serde_json::from_str(value).expect("Failed to deserialize the genesis records.")
349 }
350
351 pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
356 let mut file = File::open(path).expect("Failed to open genesis config file.");
357 let mut json_str = String::new();
358 file.read_to_string(&mut json_str)
359 .expect("Failed to read the genesis config file to string. ");
360 let json_str_without_comments: String =
361 unc_config_utils::strip_comments_from_json_str(&json_str)
362 .expect("Failed to strip comments from Genesis config file.");
363 serde_json::from_str(&json_str_without_comments)
364 .expect("Failed to deserialize the genesis records.")
365 }
366
367 pub fn to_file<P: AsRef<Path>>(&self, path: P) {
369 std::fs::write(
370 path,
371 serde_json::to_vec_pretty(self).expect("Error serializing the genesis records."),
372 )
373 .expect("Failed to create / write a genesis records file.");
374 }
375}
376
377struct RecordsProcessor<F> {
382 sink: F,
383}
384
385impl<'de, F: FnMut(StateRecord)> Visitor<'de> for RecordsProcessor<&'_ mut F> {
386 type Value = ();
387
388 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
389 formatter.write_str(
390 "either:\
391 1. array of StateRecord\
392 2. map with records field which is array of StateRecord",
393 )
394 }
395
396 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
397 where
398 A: SeqAccess<'de>,
399 {
400 while let Some(record) = seq.next_element::<StateRecord>()? {
401 (self.sink)(record)
402 }
403 Ok(())
404 }
405
406 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
407 where
408 A: MapAccess<'de>,
409 {
410 let mut me = Some(self);
411 let mut has_records_field = false;
412 while let Some(key) = map.next_key::<String>()? {
413 match key.as_str() {
414 "records" => {
415 let me =
416 me.take().ok_or_else(|| de::Error::custom("duplicate field: records"))?;
417 map.next_value_seed(me)?;
418 has_records_field = true;
419 }
420 _ => {
421 map.next_value::<IgnoredAny>()?;
422 }
423 }
424 }
425 if has_records_field {
426 Ok(())
427 } else {
428 Err(de::Error::custom("missing field: records"))
429 }
430 }
431}
432
433impl<'de, F: FnMut(StateRecord)> DeserializeSeed<'de> for RecordsProcessor<&'_ mut F> {
434 type Value = ();
435
436 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
437 where
438 D: Deserializer<'de>,
439 {
440 deserializer.deserialize_seq(self)
441 }
442}
443
444pub fn stream_records_from_file(
446 reader: impl Read,
447 mut callback: impl FnMut(StateRecord),
448) -> serde_json::Result<()> {
449 let reader_without_comments = unc_config_utils::strip_comments_from_json_reader(reader);
450 let mut deserializer = serde_json::Deserializer::from_reader(reader_without_comments);
451 let records_processor = RecordsProcessor { sink: &mut callback };
452 deserializer.deserialize_any(records_processor)
453}
454
455pub struct GenesisJsonHasher {
456 digest: sha2::Sha256,
457}
458
459impl GenesisJsonHasher {
460 pub fn new() -> Self {
461 Self { digest: sha2::Sha256::new() }
462 }
463
464 pub fn process_config(&mut self, config: &GenesisConfig) {
465 let mut ser = Serializer::pretty(&mut self.digest);
466 config.serialize(&mut ser).expect("Error serializing the genesis config.");
467 }
468
469 pub fn process_record(&mut self, record: &StateRecord) {
470 let mut ser = Serializer::pretty(&mut self.digest);
471 record.serialize(&mut ser).expect("Error serializing the genesis record.");
472 }
473
474 pub fn process_state_roots(&mut self, state_roots: &[StateRoot]) {
475 let mut ser = Serializer::pretty(&mut self.digest);
481 state_roots.serialize(&mut ser).expect("Error serializing the state roots.");
482 }
483
484 pub fn process_genesis(&mut self, genesis: &Genesis) {
485 self.process_config(&genesis.config);
486 match &genesis.contents {
487 GenesisContents::StateRoots { state_roots } => {
488 self.process_state_roots(state_roots);
489 }
490 _ => {
491 genesis.for_each_record(|record: &StateRecord| {
492 self.process_record(record);
493 });
494 }
495 }
496 }
497
498 pub fn finalize(self) -> CryptoHash {
499 CryptoHash(self.digest.finalize().into())
500 }
501}
502
503pub enum GenesisValidationMode {
504 Full,
505 UnsafeFast,
506}
507
508impl Genesis {
509 pub fn new(config: GenesisConfig, records: GenesisRecords) -> Result<Self, ValidationError> {
510 Self::new_validated(config, records, GenesisValidationMode::Full)
511 }
512
513 pub fn new_with_path<P: AsRef<Path>>(
514 config: GenesisConfig,
515 records_file: P,
516 ) -> Result<Self, ValidationError> {
517 Self::new_with_path_validated(config, records_file, GenesisValidationMode::Full)
518 }
519
520 pub fn from_file<P: AsRef<Path>>(
523 path: P,
524 genesis_validation: GenesisValidationMode,
525 ) -> Result<Self, ValidationError> {
526 let mut file = File::open(&path).map_err(|e| ValidationError::GenesisFileError {
527 error_message: format!(
528 "Could not open genesis config file at path {}: {:?}",
529 &path.as_ref().display(),
530 e,
531 ),
532 })?;
533
534 let mut json_str = String::new();
535 file.read_to_string(&mut json_str).map_err(|e| ValidationError::GenesisFileError {
536 error_message: format!("Failed to read genesis config file to string: {:?}", e),
537 })?;
538
539 let json_str_without_comments = unc_config_utils::strip_comments_from_json_str(&json_str)
540 .map_err(|e| ValidationError::GenesisFileError {
541 error_message: format!("Failed to strip comments from genesis config file: {:?}", e),
542 })?;
543
544 let genesis = serde_json::from_str::<Genesis>(&json_str_without_comments).map_err(|e| {
545 ValidationError::GenesisFileError {
546 error_message: format!("Failed to deserialize the genesis records: {:?}", e),
547 }
548 })?;
549
550 genesis.validate(genesis_validation)?;
551 Ok(genesis)
552 }
553
554 pub fn from_files<P1, P2>(
556 config_path: P1,
557 records_path: P2,
558 genesis_validation: GenesisValidationMode,
559 ) -> Result<Self, ValidationError>
560 where
561 P1: AsRef<Path>,
562 P2: AsRef<Path>,
563 {
564 let genesis_config = GenesisConfig::from_file(config_path).map_err(|error| {
565 let error_message = error.to_string();
566 ValidationError::GenesisFileError { error_message: error_message }
567 })?;
568 Self::new_with_path_validated(genesis_config, records_path, genesis_validation)
569 }
570
571 pub fn new_from_state_roots(config: GenesisConfig, state_roots: Vec<StateRoot>) -> Self {
572 Self { config, contents: GenesisContents::StateRoots { state_roots } }
573 }
574
575 fn new_validated(
576 config: GenesisConfig,
577 records: GenesisRecords,
578 genesis_validation: GenesisValidationMode,
579 ) -> Result<Self, ValidationError> {
580 let genesis = Self { config, contents: GenesisContents::Records { records } };
581 genesis.validate(genesis_validation)?;
582 Ok(genesis)
583 }
584
585 fn new_with_path_validated<P: AsRef<Path>>(
586 config: GenesisConfig,
587 records_file: P,
588 genesis_validation: GenesisValidationMode,
589 ) -> Result<Self, ValidationError> {
590 let genesis = Self {
591 config,
592 contents: GenesisContents::RecordsFile {
593 records_file: records_file.as_ref().to_path_buf(),
594 },
595 };
596 genesis.validate(genesis_validation)?;
597 Ok(genesis)
598 }
599
600 pub fn validate(
601 &self,
602 genesis_validation: GenesisValidationMode,
603 ) -> Result<(), ValidationError> {
604 match genesis_validation {
605 GenesisValidationMode::Full => validate_genesis(self),
606 GenesisValidationMode::UnsafeFast => {
607 warn!(target: "genesis", "Skipped genesis validation");
608 Ok(())
609 }
610 }
611 }
612
613 pub fn to_file<P: AsRef<Path>>(&self, path: P) {
615 std::fs::write(
616 path,
617 serde_json::to_vec_pretty(self).expect("Error serializing the genesis config."),
618 )
619 .expect("Failed to create / write a genesis config file.");
620 }
621
622 pub fn json_hash(&self) -> CryptoHash {
625 let mut hasher = GenesisJsonHasher::new();
626 hasher.process_genesis(self);
627 hasher.finalize()
628 }
629
630 pub fn for_each_record(&self, mut callback: impl FnMut(&StateRecord)) {
633 match &self.contents {
634 GenesisContents::Records { records } => {
635 for record in &records.0 {
636 callback(record);
637 }
638 }
639 GenesisContents::RecordsFile { records_file } => {
640 let callback_move = |record: StateRecord| {
641 callback(&record);
642 };
643 let reader = BufReader::new(
644 File::open(&records_file).expect("error while opening records file"),
645 );
646 stream_records_from_file(reader, callback_move)
647 .expect("error while streaming records");
648 }
649 GenesisContents::StateRoots { .. } => {
650 unreachable!("Cannot iterate through records when genesis uses state roots");
651 }
652 }
653 }
654
655 pub fn force_read_records(&mut self) -> &mut GenesisRecords {
664 match &self.contents {
665 GenesisContents::RecordsFile { records_file } => {
666 self.contents =
667 GenesisContents::Records { records: GenesisRecords::from_file(records_file) };
668 }
669 GenesisContents::Records { .. } => {}
670 GenesisContents::StateRoots { .. } => {
671 unreachable!("Cannot iterate through records when genesis uses state roots");
672 }
673 }
674 match &mut self.contents {
675 GenesisContents::Records { records } => records,
676 _ => {
677 unreachable!("Records should have been set previously");
678 }
679 }
680 }
681}
682
683#[derive(Debug, Default)]
685pub struct GenesisChangeConfig {
686 pub select_account_ids: Option<Vec<AccountId>>,
687 pub whitelist_validators: Option<HashSet<AccountId>>,
688}
689
690impl GenesisChangeConfig {
691 pub fn with_select_account_ids(mut self, select_account_ids: Option<Vec<AccountId>>) -> Self {
692 self.select_account_ids = select_account_ids;
693 self
694 }
695
696 pub fn with_whitelist_validators(
697 mut self,
698 whitelist_validators: Option<Vec<AccountId>>,
699 ) -> Self {
700 self.whitelist_validators = match whitelist_validators {
701 None => None,
702 Some(whitelist) => Some(whitelist.into_iter().collect::<HashSet<AccountId>>()),
703 };
704 self
705 }
706}
707
708#[derive(serde::Serialize, serde::Deserialize, Debug)]
716pub struct ProtocolConfigView {
717 pub protocol_version: ProtocolVersion,
719 pub genesis_time: DateTime<Utc>,
721 pub chain_id: String,
724 pub genesis_height: BlockHeight,
726 pub num_block_producer_seats: NumSeats,
728 pub num_block_producer_seats_per_shard: Vec<NumSeats>,
730 pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
732 pub protocol_upgrade_pledge_threshold: Rational32,
734 pub epoch_length: BlockHeightDelta,
736 pub gas_limit: Gas,
738 #[serde(with = "dec_format")]
740 pub min_gas_price: Balance,
741 #[serde(with = "dec_format")]
743 pub max_gas_price: Balance,
744 pub block_producer_kickout_threshold: u8,
746 pub chunk_producer_kickout_threshold: u8,
748 pub online_min_threshold: Rational32,
750 pub online_max_threshold: Rational32,
752 pub gas_price_adjustment_rate: Rational32,
754 pub runtime_config: RuntimeConfigView,
756 pub transaction_validity_period: NumBlocks,
758 pub protocol_reward_rate: Rational32,
760 pub max_inflation_rate: Rational32,
762 pub num_blocks_per_year: NumBlocks,
764 pub protocol_treasury_account: AccountId,
766 #[serde(with = "dec_format")]
768 pub fishermen_threshold: Balance,
769 pub minimum_pledge_divisor: u64,
771 pub max_kickout_pledge_perc: u8,
773 pub minimum_pledge_ratio: Rational32,
775 pub minimum_validators_per_shard: NumSeats,
777 pub num_chunk_only_producer_seats: NumSeats,
779 pub shard_layout: ShardLayout,
781}
782
783pub struct ProtocolConfig {
784 pub genesis_config: GenesisConfig,
785 pub runtime_config: RuntimeConfig,
786}
787
788impl From<ProtocolConfig> for ProtocolConfigView {
789 fn from(protocol_config: ProtocolConfig) -> Self {
790 let ProtocolConfig { genesis_config, runtime_config } = protocol_config;
791
792 ProtocolConfigView {
793 protocol_version: genesis_config.protocol_version,
794 genesis_time: genesis_config.genesis_time,
795 chain_id: genesis_config.chain_id,
796 genesis_height: genesis_config.genesis_height,
797 num_block_producer_seats: genesis_config.num_block_producer_seats,
798 num_block_producer_seats_per_shard: genesis_config.num_block_producer_seats_per_shard,
799 avg_hidden_validator_seats_per_shard: genesis_config
800 .avg_hidden_validator_seats_per_shard,
801 protocol_upgrade_pledge_threshold: genesis_config.protocol_upgrade_pledge_threshold,
802 epoch_length: genesis_config.epoch_length,
803 gas_limit: genesis_config.gas_limit,
804 min_gas_price: genesis_config.min_gas_price,
805 max_gas_price: genesis_config.max_gas_price,
806 block_producer_kickout_threshold: genesis_config.block_producer_kickout_threshold,
807 chunk_producer_kickout_threshold: genesis_config.chunk_producer_kickout_threshold,
808 online_min_threshold: genesis_config.online_min_threshold,
809 online_max_threshold: genesis_config.online_max_threshold,
810 gas_price_adjustment_rate: genesis_config.gas_price_adjustment_rate,
811 runtime_config: RuntimeConfigView::from(runtime_config),
812 transaction_validity_period: genesis_config.transaction_validity_period,
813 protocol_reward_rate: genesis_config.protocol_reward_rate,
814 max_inflation_rate: genesis_config.max_inflation_rate,
815 num_blocks_per_year: genesis_config.num_blocks_per_year,
816 protocol_treasury_account: genesis_config.protocol_treasury_account,
817 fishermen_threshold: genesis_config.fishermen_threshold,
818 minimum_pledge_divisor: genesis_config.minimum_pledge_divisor,
819 max_kickout_pledge_perc: genesis_config.max_kickout_pledge_perc,
820 minimum_pledge_ratio: genesis_config.minimum_pledge_ratio,
821 minimum_validators_per_shard: genesis_config.minimum_validators_per_shard,
822 num_chunk_only_producer_seats: genesis_config.num_chunk_only_producer_seats,
823 shard_layout: genesis_config.shard_layout,
824 }
825 }
826}
827
828pub fn get_initial_supply(records: &[StateRecord]) -> Balance {
829 let mut total_supply = 0;
830 for record in records {
831 if let StateRecord::Account { account, .. } = record {
832 total_supply += account.amount() + account.pledging();
833 }
834 }
835 total_supply
836}
837
838#[cfg(test)]
839mod test {
840 use crate::genesis_config::RecordsProcessor;
841 use crate::{Genesis, GenesisValidationMode};
842 use serde::Deserializer;
843 use unc_primitives::state_record::StateRecord;
844
845 fn stream_records_from_json_str(genesis: &str) -> serde_json::Result<()> {
846 let mut deserializer = serde_json::Deserializer::from_reader(genesis.as_bytes());
847 let records_processor = RecordsProcessor { sink: &mut |_record: StateRecord| {} };
848 deserializer.deserialize_any(records_processor)
849 }
850
851 #[test]
852 fn test_genesis_with_empty_records() {
853 let genesis = r#"{
854 "a": [1, 2],
855 "b": "random",
856 "records": []
857 }"#;
858 stream_records_from_json_str(genesis).expect("error reading empty records");
859 }
860
861 #[test]
862 #[should_panic(expected = "missing field: records")]
863 fn test_genesis_with_no_records() {
864 let genesis = r#"{
865 "a": [1, 2],
866 "b": "random"
867 }"#;
868 stream_records_from_json_str(genesis).unwrap();
869 }
870
871 #[test]
872 #[should_panic(expected = "duplicate field: records")]
873 fn test_genesis_with_several_records_fields() {
874 let genesis = r#"{
875 "a": [1, 2],
876 "records": [{
877 "Account": {
878 "account_id": "01.unc",
879 "account": {
880 "amount": "49999999958035075000000000",
881 "pledging": "0",
882 "code_hash": "11111111111111111111111111111111",
883 "storage_usage": 264
884 }
885 }
886 }],
887 "b": "random",
888 "records": [{
889 "Account": {
890 "account_id": "01.unc",
891 "account": {
892 "amount": "49999999958035075000000000",
893 "pledging": "0",
894 "code_hash": "11111111111111111111111111111111",
895 "storage_usage": 264
896 }
897 }
898 }]
899 }"#;
900 stream_records_from_json_str(genesis).unwrap();
901 }
902
903 #[test]
904 fn test_genesis_with_fields_after_records() {
905 let genesis = r#"{
906 "a": [1, 2],
907 "b": "random",
908 "records": [
909 {
910 "Account": {
911 "account_id": "01.unc",
912 "account": {
913 "amount": "49999999958035075000000000",
914 "pledging": "0",
915 "code_hash": "11111111111111111111111111111111",
916 "storage_usage": 264
917 }
918 }
919 }
920 ],
921 "c": {
922 "d": 1,
923 "e": []
924 }
925 }"#;
926 stream_records_from_json_str(genesis).expect("error reading records with a field after");
927 }
928
929 #[test]
930 fn test_genesis_with_fields_before_records() {
931 let genesis = r#"{
932 "a": [1, 2],
933 "b": "random",
934 "c": {
935 "d": 1,
936 "e": []
937 },
938 "records": [
939 {
940 "Account": {
941 "account_id": "01.unc",
942 "account": {
943 "amount": "49999999958035075000000000",
944 "pledging": "0",
945 "code_hash": "11111111111111111111111111111111",
946 "storage_usage": 264
947 }
948 }
949 }
950 ]
951 }"#;
952 stream_records_from_json_str(genesis).expect("error reading records from genesis");
953 }
954
955 #[test]
956 fn test_genesis_with_several_records() {
957 let genesis = r#"{
958 "a": [1, 2],
959 "b": "random",
960 "c": {
961 "d": 1,
962 "e": []
963 },
964 "records": [
965 {
966 "Account": {
967 "account_id": "01.unc",
968 "account": {
969 "amount": "49999999958035075000000000",
970 "pledging": "0",
971 "code_hash": "11111111111111111111111111111111",
972 "storage_usage": 264
973 }
974 }
975 },
976 {
977 "Account": {
978 "account_id": "01.unc",
979 "account": {
980 "amount": "49999999958035075000000000",
981 "pledging": "0",
982 "code_hash": "11111111111111111111111111111111",
983 "storage_usage": 264
984 }
985 }
986 }
987 ]
988 }"#;
989 stream_records_from_json_str(genesis).expect("error reading records from genesis");
990 }
991
992 #[test]
993 fn test_loading_localnet_genesis() {
994 let genesis_str = r#"{
995 "protocol_version": 57,
996 "genesis_time": "2022-11-15T05:17:59.578706Z",
997 "chain_id": "localnet",
998 "genesis_height": 0,
999 "num_block_producer_seats": 50,
1000 "num_block_producer_seats_per_shard": [
1001 50
1002 ],
1003 "avg_hidden_validator_seats_per_shard": [
1004 0
1005 ],
1006 "protocol_upgrade_pledge_threshold": [
1007 4,
1008 5
1009 ],
1010 "protocol_upgrade_num_epochs": 2,
1011 "epoch_length": 60,
1012 "gas_limit": 1000000000000000,
1013 "min_gas_price": "100000000",
1014 "max_gas_price": "10000000000000000000000",
1015 "block_producer_kickout_threshold": 90,
1016 "chunk_producer_kickout_threshold": 90,
1017 "online_min_threshold": [
1018 9,
1019 10
1020 ],
1021 "online_max_threshold": [
1022 99,
1023 100
1024 ],
1025 "gas_price_adjustment_rate": [
1026 1,
1027 100
1028 ],
1029 "validators": [
1030 {
1031 "account_id": "test.unc",
1032 "public_key": "ed25519:Gc4yTakj3QVm5T9XpsFNooVKBxXcYnhnuQdGMXf5Hjcf",
1033 "amount": "50000000000000000000000000000000"
1034 }
1035 ],
1036 "transaction_validity_period": 100,
1037 "protocol_reward_rate": [
1038 1,
1039 10
1040 ],
1041 "max_inflation_rate": [
1042 1,
1043 20
1044 ],
1045 "total_supply": "2050000000000000000000000000000000",
1046 "num_blocks_per_year": 31536000,
1047 "protocol_treasury_account": "test.unc",
1048 "fishermen_threshold": "10000000000000000000000000",
1049 "minimum_pledge_divisor": 10,
1050 "num_chunk_only_producer_seats": 300,
1051 "minimum_validators_per_shard": 1,
1052 "max_kickout_pledge_perc": 100,
1053 "minimum_pledge_ratio": [
1054 1,
1055 6250
1056 ],
1057 "use_production_config": false,
1058 "records": [
1059 {
1060 "Account": {
1061 "account_id": "test.unc",
1062 "account": {
1063 "amount": "1000000000000000000000000000000000",
1064 "pledging": "50000000000000000000000000000000",
1065 "code_hash": "11111111111111111111111111111111",
1066 "storage_usage": 0,
1067 "version": "V1"
1068 }
1069 }
1070 },
1071 {
1072 "AccessKey": {
1073 "account_id": "test.unc",
1074 "public_key": "ed25519:Gc4yTakj3QVm5T9XpsFNooVKBxXcYnhnuQdGMXf5Hjcf",
1075 "access_key": {
1076 "nonce": 0,
1077 "permission": "FullAccess"
1078 }
1079 }
1080 },
1081 {
1082 "Account": {
1083 "account_id": "unc",
1084 "account": {
1085 "amount": "1000000000000000000000000000000000",
1086 "pledging": "0",
1087 "code_hash": "11111111111111111111111111111111",
1088 "storage_usage": 0,
1089 "version": "V1"
1090 }
1091 }
1092 },
1093 {
1094 "AccessKey": {
1095 "account_id": "unc",
1096 "public_key": "ed25519:546XB2oHhj7PzUKHiH9Xve3Ze5q1JiW2WTh6abXFED3c",
1097 "access_key": {
1098 "nonce": 0,
1099 "permission": "FullAccess"
1100 }
1101 }
1102 }
1103 ]
1104 }"#;
1105 let genesis =
1106 serde_json::from_str::<Genesis>(&genesis_str).expect("Failed to deserialize Genesis");
1107 genesis.validate(GenesisValidationMode::Full).expect("Failed to validate Genesis");
1108 }
1109
1110 #[test]
1111 fn test_loading_localnet_genesis_without_records() {
1112 let genesis_str = r#"{
1113 "protocol_version": 57,
1114 "genesis_time": "2022-11-15T05:17:59.578706Z",
1115 "chain_id": "localnet",
1116 "genesis_height": 0,
1117 "num_block_producer_seats": 50,
1118 "num_block_producer_seats_per_shard": [
1119 50
1120 ],
1121 "avg_hidden_validator_seats_per_shard": [
1122 0
1123 ],
1124 "protocol_upgrade_pledge_threshold": [
1125 4,
1126 5
1127 ],
1128 "protocol_upgrade_num_epochs": 2,
1129 "epoch_length": 60,
1130 "gas_limit": 1000000000000000,
1131 "min_gas_price": "100000000",
1132 "max_gas_price": "10000000000000000000000",
1133 "block_producer_kickout_threshold": 90,
1134 "chunk_producer_kickout_threshold": 90,
1135 "online_min_threshold": [
1136 9,
1137 10
1138 ],
1139 "online_max_threshold": [
1140 99,
1141 100
1142 ],
1143 "gas_price_adjustment_rate": [
1144 1,
1145 100
1146 ],
1147 "validators": [
1148 {
1149 "account_id": "test.unc",
1150 "public_key": "ed25519:Gc4yTakj3QVm5T9XpsFNooVKBxXcYnhnuQdGMXf5Hjcf",
1151 "amount": "50000000000000000000000000000000"
1152 }
1153 ],
1154 "transaction_validity_period": 100,
1155 "protocol_reward_rate": [
1156 1,
1157 10
1158 ],
1159 "max_inflation_rate": [
1160 1,
1161 20
1162 ],
1163 "total_supply": "2050000000000000000000000000000000",
1164 "num_blocks_per_year": 31536000,
1165 "protocol_treasury_account": "test.unc",
1166 "fishermen_threshold": "10000000000000000000000000",
1167 "minimum_pledge_divisor": 10,
1168 "num_chunk_only_producer_seats": 300,
1169 "minimum_validators_per_shard": 1,
1170 "max_kickout_pledge_perc": 100,
1171 "minimum_pledge_ratio": [
1172 1,
1173 6250
1174 ],
1175 "use_production_config": false
1176 }"#;
1177 let _genesis =
1178 serde_json::from_str::<Genesis>(&genesis_str).expect("Failed to deserialize Genesis");
1179 }
1180}