iota_swarm_config/
network_config_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    num::NonZeroUsize,
7    path::{Path, PathBuf},
8    sync::Arc,
9    time::Duration,
10};
11
12use fastcrypto::traits::KeyPair;
13use iota_config::{
14    IOTA_GENESIS_MIGRATION_TX_DATA_FILENAME,
15    genesis::{TokenAllocation, TokenDistributionScheduleBuilder},
16    node::AuthorityOverloadConfig,
17};
18use iota_genesis_builder::genesis_build_effects::GenesisBuildEffects;
19use iota_macros::nondeterministic;
20use iota_types::{
21    base_types::{AuthorityName, IotaAddress},
22    committee::{Committee, ProtocolVersion},
23    crypto::{AccountKeyPair, PublicKey, get_key_pair_from_rng},
24    object::Object,
25    supported_protocol_versions::SupportedProtocolVersions,
26    traffic_control::{PolicyConfig, RemoteFirewallConfig},
27};
28use rand::rngs::OsRng;
29
30use crate::{
31    genesis_config::{
32        AccountConfig, DEFAULT_GAS_AMOUNT, GenesisConfig, ValidatorGenesisConfig,
33        ValidatorGenesisConfigBuilder,
34    },
35    network_config::NetworkConfig,
36    node_config_builder::ValidatorConfigBuilder,
37};
38
39pub enum CommitteeConfig {
40    Size(NonZeroUsize),
41    Validators(Vec<ValidatorGenesisConfig>),
42    AccountKeys(Vec<AccountKeyPair>),
43    /// Indicates that a committee should be deterministically generated, using
44    /// the provided rng as a source of randomness as well as generating
45    /// deterministic network port information.
46    Deterministic((NonZeroUsize, Option<Vec<AccountKeyPair>>)),
47}
48
49pub type SupportedProtocolVersionsCallback = Arc<
50    dyn Fn(
51            usize,                 // validator idx
52            Option<AuthorityName>, // None for fullnode
53        ) -> SupportedProtocolVersions
54        + Send
55        + Sync
56        + 'static,
57>;
58
59#[derive(Clone)]
60pub enum ProtocolVersionsConfig {
61    // use SYSTEM_DEFAULT
62    Default,
63    // Use one range for all validators.
64    Global(SupportedProtocolVersions),
65    // A closure that returns the versions for each validator.
66    // TODO: This doesn't apply to fullnodes.
67    PerValidator(SupportedProtocolVersionsCallback),
68}
69
70pub type StateAccumulatorEnabledCallback = Arc<dyn Fn(usize) -> bool + Send + Sync + 'static>;
71
72#[derive(Clone)]
73pub enum StateAccumulatorV1EnabledConfig {
74    Global(bool),
75    PerValidator(StateAccumulatorEnabledCallback),
76}
77
78pub struct ConfigBuilder<R = OsRng> {
79    rng: Option<R>,
80    config_directory: PathBuf,
81    supported_protocol_versions_config: Option<ProtocolVersionsConfig>,
82    committee: CommitteeConfig,
83    genesis_config: Option<GenesisConfig>,
84    reference_gas_price: Option<u64>,
85    additional_objects: Vec<Object>,
86    jwk_fetch_interval: Option<Duration>,
87    num_unpruned_validators: Option<usize>,
88    authority_overload_config: Option<AuthorityOverloadConfig>,
89    data_ingestion_dir: Option<PathBuf>,
90    policy_config: Option<PolicyConfig>,
91    firewall_config: Option<RemoteFirewallConfig>,
92    max_submit_position: Option<usize>,
93    submit_delay_step_override_millis: Option<u64>,
94    state_accumulator_config: Option<StateAccumulatorV1EnabledConfig>,
95    empty_validator_genesis: bool,
96}
97
98impl ConfigBuilder {
99    pub fn new<P: AsRef<Path>>(config_directory: P) -> Self {
100        Self {
101            rng: Some(OsRng),
102            config_directory: config_directory.as_ref().into(),
103            supported_protocol_versions_config: None,
104            committee: CommitteeConfig::Size(NonZeroUsize::new(1).unwrap()),
105            genesis_config: None,
106            reference_gas_price: None,
107            additional_objects: vec![],
108            jwk_fetch_interval: None,
109            num_unpruned_validators: None,
110            authority_overload_config: None,
111            data_ingestion_dir: None,
112            policy_config: None,
113            firewall_config: None,
114            max_submit_position: None,
115            submit_delay_step_override_millis: None,
116            state_accumulator_config: Some(StateAccumulatorV1EnabledConfig::Global(true)),
117            empty_validator_genesis: false,
118        }
119    }
120
121    pub fn new_with_temp_dir() -> Self {
122        Self::new(nondeterministic!(tempfile::tempdir().unwrap()).into_path())
123    }
124}
125
126impl<R> ConfigBuilder<R> {
127    pub fn committee(mut self, committee: CommitteeConfig) -> Self {
128        self.committee = committee;
129        self
130    }
131
132    pub fn committee_size(mut self, committee_size: NonZeroUsize) -> Self {
133        self.committee = CommitteeConfig::Size(committee_size);
134        self
135    }
136
137    pub fn deterministic_committee_size(mut self, committee_size: NonZeroUsize) -> Self {
138        self.committee = CommitteeConfig::Deterministic((committee_size, None));
139        self
140    }
141
142    pub fn deterministic_committee_validators(mut self, keys: Vec<AccountKeyPair>) -> Self {
143        self.committee = CommitteeConfig::Deterministic((
144            NonZeroUsize::new(keys.len()).expect("Validator keys should be non empty"),
145            Some(keys),
146        ));
147        self
148    }
149
150    pub fn with_validator_account_keys(mut self, keys: Vec<AccountKeyPair>) -> Self {
151        self.committee = CommitteeConfig::AccountKeys(keys);
152        self
153    }
154
155    pub fn with_validators(mut self, validators: Vec<ValidatorGenesisConfig>) -> Self {
156        self.committee = CommitteeConfig::Validators(validators);
157        self
158    }
159
160    pub fn with_genesis_config(mut self, genesis_config: GenesisConfig) -> Self {
161        assert!(self.genesis_config.is_none(), "Genesis config already set");
162        self.genesis_config = Some(genesis_config);
163        self
164    }
165
166    pub fn with_num_unpruned_validators(mut self, n: usize) -> Self {
167        self.num_unpruned_validators = Some(n);
168        self
169    }
170
171    pub fn with_jwk_fetch_interval(mut self, i: Duration) -> Self {
172        self.jwk_fetch_interval = Some(i);
173        self
174    }
175
176    pub fn with_data_ingestion_dir(mut self, path: PathBuf) -> Self {
177        self.data_ingestion_dir = Some(path);
178        self
179    }
180
181    pub fn with_reference_gas_price(mut self, reference_gas_price: u64) -> Self {
182        self.reference_gas_price = Some(reference_gas_price);
183        self
184    }
185
186    pub fn with_accounts(mut self, accounts: Vec<AccountConfig>) -> Self {
187        self.get_or_init_genesis_config().accounts = accounts;
188        self
189    }
190
191    pub fn with_chain_start_timestamp_ms(mut self, chain_start_timestamp_ms: u64) -> Self {
192        self.get_or_init_genesis_config()
193            .parameters
194            .chain_start_timestamp_ms = chain_start_timestamp_ms;
195        self
196    }
197
198    pub fn with_objects<I: IntoIterator<Item = Object>>(mut self, objects: I) -> Self {
199        self.additional_objects.extend(objects);
200        self
201    }
202
203    pub fn with_epoch_duration(mut self, epoch_duration_ms: u64) -> Self {
204        self.get_or_init_genesis_config()
205            .parameters
206            .epoch_duration_ms = epoch_duration_ms;
207        self
208    }
209
210    pub fn with_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self {
211        self.get_or_init_genesis_config()
212            .parameters
213            .protocol_version = protocol_version;
214        self
215    }
216
217    pub fn with_supported_protocol_versions(mut self, c: SupportedProtocolVersions) -> Self {
218        self.supported_protocol_versions_config = Some(ProtocolVersionsConfig::Global(c));
219        self
220    }
221
222    pub fn with_supported_protocol_version_callback(
223        mut self,
224        func: SupportedProtocolVersionsCallback,
225    ) -> Self {
226        self.supported_protocol_versions_config = Some(ProtocolVersionsConfig::PerValidator(func));
227        self
228    }
229
230    pub fn with_supported_protocol_versions_config(mut self, c: ProtocolVersionsConfig) -> Self {
231        self.supported_protocol_versions_config = Some(c);
232        self
233    }
234
235    pub fn with_state_accumulator_callback(
236        mut self,
237        func: StateAccumulatorEnabledCallback,
238    ) -> Self {
239        self.state_accumulator_config = Some(StateAccumulatorV1EnabledConfig::PerValidator(func));
240        self
241    }
242
243    pub fn with_state_accumulator_config(mut self, c: StateAccumulatorV1EnabledConfig) -> Self {
244        self.state_accumulator_config = Some(c);
245        self
246    }
247
248    pub fn with_authority_overload_config(mut self, c: AuthorityOverloadConfig) -> Self {
249        self.authority_overload_config = Some(c);
250        self
251    }
252
253    pub fn with_policy_config(mut self, config: Option<PolicyConfig>) -> Self {
254        self.policy_config = config;
255        self
256    }
257
258    pub fn with_firewall_config(mut self, config: Option<RemoteFirewallConfig>) -> Self {
259        self.firewall_config = config;
260        self
261    }
262
263    pub fn with_max_submit_position(mut self, max_submit_position: usize) -> Self {
264        self.max_submit_position = Some(max_submit_position);
265        self
266    }
267
268    pub fn with_submit_delay_step_override_millis(
269        mut self,
270        submit_delay_step_override_millis: u64,
271    ) -> Self {
272        self.submit_delay_step_override_millis = Some(submit_delay_step_override_millis);
273        self
274    }
275
276    pub fn rng<N: rand::RngCore + rand::CryptoRng>(self, rng: N) -> ConfigBuilder<N> {
277        ConfigBuilder {
278            rng: Some(rng),
279            config_directory: self.config_directory,
280            supported_protocol_versions_config: self.supported_protocol_versions_config,
281            committee: self.committee,
282            genesis_config: self.genesis_config,
283            reference_gas_price: self.reference_gas_price,
284            additional_objects: self.additional_objects,
285            num_unpruned_validators: self.num_unpruned_validators,
286            jwk_fetch_interval: self.jwk_fetch_interval,
287            authority_overload_config: self.authority_overload_config,
288            data_ingestion_dir: self.data_ingestion_dir,
289            policy_config: self.policy_config,
290            firewall_config: self.firewall_config,
291            max_submit_position: self.max_submit_position,
292            submit_delay_step_override_millis: self.submit_delay_step_override_millis,
293            state_accumulator_config: self.state_accumulator_config,
294            empty_validator_genesis: self.empty_validator_genesis,
295        }
296    }
297
298    fn get_or_init_genesis_config(&mut self) -> &mut GenesisConfig {
299        if self.genesis_config.is_none() {
300            self.genesis_config = Some(GenesisConfig::for_local_testing());
301        }
302        self.genesis_config.as_mut().unwrap()
303    }
304
305    /// Avoid initializing validator genesis in memory.
306    ///
307    /// This allows callers to create the genesis blob,
308    /// and use a file pointer to configure the validators.
309    pub fn with_empty_validator_genesis(mut self) -> Self {
310        self.empty_validator_genesis = true;
311        self
312    }
313}
314
315impl<R: rand::RngCore + rand::CryptoRng> ConfigBuilder<R> {
316    // TODO right now we always randomize ports, we may want to have a default port
317    // configuration
318    pub fn build(self) -> NetworkConfig {
319        let committee = self.committee;
320
321        let mut rng = self.rng.unwrap();
322        let validators = match committee {
323            CommitteeConfig::Size(size) => {
324                // We always get fixed authority keys from this function (which is isolated from
325                // external test randomness because it uses a fixed seed). Necessary because
326                // some tests call `make_tx_certs_and_signed_effects`, which
327                // locally forges a cert using this same committee.
328                let (_, keys) = Committee::new_simple_test_committee_of_size(size.into());
329
330                keys.into_iter()
331                    .map(|authority_key| {
332                        let mut builder = ValidatorGenesisConfigBuilder::new()
333                            .with_authority_key_pair(authority_key);
334                        if let Some(rgp) = self.reference_gas_price {
335                            builder = builder.with_gas_price(rgp);
336                        }
337                        builder.build(&mut rng)
338                    })
339                    .collect::<Vec<_>>()
340            }
341
342            CommitteeConfig::Validators(v) => v,
343
344            CommitteeConfig::AccountKeys(keys) => {
345                // See above re fixed authority keys
346                let (_, authority_keys) = Committee::new_simple_test_committee_of_size(keys.len());
347                keys.into_iter()
348                    .zip(authority_keys)
349                    .map(|(account_key, authority_key)| {
350                        let mut builder = ValidatorGenesisConfigBuilder::new()
351                            .with_authority_key_pair(authority_key)
352                            .with_account_key_pair(account_key);
353                        if let Some(rgp) = self.reference_gas_price {
354                            builder = builder.with_gas_price(rgp);
355                        }
356                        builder.build(&mut rng)
357                    })
358                    .collect::<Vec<_>>()
359            }
360            CommitteeConfig::Deterministic((size, keys)) => {
361                // If no keys are provided, generate them.
362                let keys = keys.unwrap_or(
363                    (0..size.get())
364                        .map(|_| get_key_pair_from_rng(&mut rng).1)
365                        .collect(),
366                );
367
368                let mut configs = vec![];
369                for (i, key) in keys.into_iter().enumerate() {
370                    let port_offset = 8000 + i * 10;
371                    let mut builder = ValidatorGenesisConfigBuilder::new()
372                        .with_ip("127.0.0.1".to_owned())
373                        .with_account_key_pair(key)
374                        .with_deterministic_ports(port_offset as u16);
375                    if let Some(rgp) = self.reference_gas_price {
376                        builder = builder.with_gas_price(rgp);
377                    }
378                    configs.push(builder.build(&mut rng));
379                }
380                configs
381            }
382        };
383
384        let mut genesis_config = self
385            .genesis_config
386            .unwrap_or_else(GenesisConfig::for_local_testing);
387
388        let (account_keys, allocations) = genesis_config.generate_accounts(&mut rng).unwrap();
389
390        let token_distribution_schedule = {
391            let mut builder = TokenDistributionScheduleBuilder::new();
392            for allocation in allocations {
393                builder.add_allocation(allocation);
394            }
395            // Add allocations for each validator
396            for validator in &validators {
397                let account_key: PublicKey = validator.account_key_pair.public();
398                let address = IotaAddress::from(&account_key);
399                // Give each validator some gas so they can pay for their transactions.
400                let gas_coin = TokenAllocation {
401                    recipient_address: address,
402                    amount_nanos: DEFAULT_GAS_AMOUNT,
403                    staked_with_validator: None,
404                    staked_with_timelock_expiration: None,
405                };
406                let stake = TokenAllocation {
407                    recipient_address: address,
408                    amount_nanos: validator.stake,
409                    staked_with_validator: Some(address),
410                    staked_with_timelock_expiration: None,
411                };
412                builder.add_allocation(gas_coin);
413                builder.add_allocation(stake);
414            }
415            builder.build()
416        };
417
418        let GenesisBuildEffects {
419            genesis,
420            migration_tx_data,
421        } = {
422            let mut builder = iota_genesis_builder::Builder::new()
423                .with_parameters(genesis_config.parameters)
424                .add_objects(self.additional_objects);
425            for source in std::mem::take(&mut genesis_config.migration_sources) {
426                builder = builder.add_migration_source(source);
427            }
428
429            for (i, validator) in validators.iter().enumerate() {
430                let name = validator
431                    .name
432                    .clone()
433                    .unwrap_or(format!("validator-{i}").to_string());
434                let validator_info = validator.to_validator_info(name);
435                builder =
436                    builder.add_validator(validator_info.info, validator_info.proof_of_possession);
437            }
438
439            builder = builder.with_token_distribution_schedule(token_distribution_schedule);
440
441            // Add delegator to genesis builder.
442            if let Some(delegator) = genesis_config.delegator {
443                builder = builder.with_delegator(delegator);
444            }
445
446            for validator in &validators {
447                builder = builder.add_validator_signature(&validator.authority_key_pair);
448            }
449
450            builder.build()
451        };
452
453        if let Some(migration_tx_data) = migration_tx_data {
454            migration_tx_data
455                .save(
456                    self.config_directory
457                        .join(IOTA_GENESIS_MIGRATION_TX_DATA_FILENAME),
458                )
459                .expect("Should be able to save the migration data");
460        }
461
462        let validator_configs = validators
463            .into_iter()
464            .enumerate()
465            .map(|(idx, validator)| {
466                let mut builder = ValidatorConfigBuilder::new()
467                    .with_config_directory(self.config_directory.clone())
468                    .with_policy_config(self.policy_config.clone())
469                    .with_firewall_config(self.firewall_config.clone());
470
471                if let Some(max_submit_position) = self.max_submit_position {
472                    builder = builder.with_max_submit_position(max_submit_position);
473                }
474
475                if let Some(submit_delay_step_override_millis) =
476                    self.submit_delay_step_override_millis
477                {
478                    builder = builder
479                        .with_submit_delay_step_override_millis(submit_delay_step_override_millis);
480                }
481
482                if let Some(jwk_fetch_interval) = self.jwk_fetch_interval {
483                    builder = builder.with_jwk_fetch_interval(jwk_fetch_interval);
484                }
485
486                if let Some(authority_overload_config) = &self.authority_overload_config {
487                    builder =
488                        builder.with_authority_overload_config(authority_overload_config.clone());
489                }
490
491                if let Some(path) = &self.data_ingestion_dir {
492                    builder = builder.with_data_ingestion_dir(path.clone());
493                }
494
495                if let Some(spvc) = &self.supported_protocol_versions_config {
496                    let supported_versions = match spvc {
497                        ProtocolVersionsConfig::Default => {
498                            SupportedProtocolVersions::SYSTEM_DEFAULT
499                        }
500                        ProtocolVersionsConfig::Global(v) => *v,
501                        ProtocolVersionsConfig::PerValidator(func) => {
502                            func(idx, Some(validator.authority_key_pair.public().into()))
503                        }
504                    };
505                    builder = builder.with_supported_protocol_versions(supported_versions);
506                }
507                if let Some(num_unpruned_validators) = self.num_unpruned_validators {
508                    if idx < num_unpruned_validators {
509                        builder = builder.with_unpruned_checkpoints();
510                    }
511                }
512                if self.empty_validator_genesis {
513                    builder.build_without_genesis(validator)
514                } else {
515                    builder.build(validator, genesis.clone())
516                }
517            })
518            .collect();
519        NetworkConfig {
520            validator_configs,
521            genesis,
522            account_keys,
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use iota_config::node::Genesis;
530
531    #[test]
532    fn serialize_genesis_config_in_place() {
533        let dir = tempfile::TempDir::new().unwrap();
534        let network_config = crate::network_config_builder::ConfigBuilder::new(&dir).build();
535        let genesis = network_config.genesis;
536
537        let g = Genesis::new(genesis);
538
539        let mut s = serde_yaml::to_string(&g).unwrap();
540        let loaded_genesis: Genesis = serde_yaml::from_str(&s).unwrap();
541        loaded_genesis
542            .genesis()
543            .unwrap()
544            .checkpoint_contents()
545            .digest(); // cache digest before comparing.
546        assert_eq!(g, loaded_genesis);
547
548        // If both in-place and file location are provided, prefer the in-place variant
549        s.push_str("\ngenesis-file-location: path/to/file");
550        let loaded_genesis: Genesis = serde_yaml::from_str(&s).unwrap();
551        loaded_genesis
552            .genesis()
553            .unwrap()
554            .checkpoint_contents()
555            .digest(); // cache digest before comparing.
556        assert_eq!(g, loaded_genesis);
557    }
558
559    #[test]
560    fn load_genesis_config_from_file() {
561        let file = tempfile::NamedTempFile::new().unwrap();
562        let genesis_config = Genesis::new_from_file(file.path());
563
564        let dir = tempfile::TempDir::new().unwrap();
565        let network_config = crate::network_config_builder::ConfigBuilder::new(&dir).build();
566        let genesis = network_config.genesis;
567        genesis.save(file.path()).unwrap();
568
569        let loaded_genesis = genesis_config.genesis().unwrap();
570        loaded_genesis.checkpoint_contents().digest(); // cache digest before comparing.
571        assert_eq!(&genesis, loaded_genesis);
572    }
573}
574
575#[cfg(test)]
576mod test {
577    use std::{collections::HashSet, sync::Arc};
578
579    use iota_config::genesis::Genesis;
580    use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
581    use iota_types::{
582        epoch_data::EpochData, gas::IotaGasStatus, in_memory_storage::InMemoryStorage,
583        iota_system_state::IotaSystemStateTrait, metrics::LimitsMetrics,
584        transaction::CheckedInputObjects,
585    };
586
587    #[test]
588    fn roundtrip() {
589        let dir = tempfile::TempDir::new().unwrap();
590        let network_config = crate::network_config_builder::ConfigBuilder::new(&dir).build();
591        let genesis = network_config.genesis;
592
593        let s = serde_yaml::to_string(&genesis).unwrap();
594        let from_s: Genesis = serde_yaml::from_str(&s).unwrap();
595        // cache the digest so that the comparison succeeds.
596        from_s.checkpoint_contents().digest();
597        assert_eq!(genesis, from_s);
598    }
599
600    #[test]
601    fn genesis_transaction() {
602        let builder = crate::network_config_builder::ConfigBuilder::new_with_temp_dir();
603        let network_config = builder.build();
604        let genesis = network_config.genesis;
605        let protocol_version =
606            ProtocolVersion::new(genesis.iota_system_object().protocol_version());
607        let protocol_config = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown);
608
609        let genesis_transaction = genesis.transaction().clone();
610
611        let genesis_digest = *genesis_transaction.digest();
612
613        let silent = true;
614        let executor = iota_execution::executor(&protocol_config, silent, None)
615            .expect("Creating an executor should not fail here");
616
617        // Use a throwaway metrics registry for genesis transaction execution.
618        let registry = prometheus::Registry::new();
619        let metrics = Arc::new(LimitsMetrics::new(&registry));
620        let expensive_checks = false;
621        let certificate_deny_set = HashSet::new();
622        let epoch = EpochData::new_test();
623        let transaction_data = &genesis_transaction.data().intent_message().value;
624        let (kind, signer, _) = transaction_data.execution_parts();
625        let input_objects = CheckedInputObjects::new_for_genesis(vec![]);
626
627        let (_inner_temp_store, _, effects, _execution_error) = executor
628            .execute_transaction_to_effects(
629                &InMemoryStorage::new(Vec::new()),
630                &protocol_config,
631                metrics,
632                expensive_checks,
633                &certificate_deny_set,
634                &epoch.epoch_id(),
635                epoch.epoch_start_timestamp(),
636                input_objects,
637                vec![],
638                IotaGasStatus::new_unmetered(),
639                kind,
640                signer,
641                genesis_digest,
642                &mut None,
643            );
644
645        assert_eq!(&effects, genesis.effects());
646    }
647}