tempo_xtask/
genesis_args.rs

1use alloy::{
2    genesis::{ChainConfig, Genesis, GenesisAccount},
3    primitives::{Address, U256, address},
4    signers::{local::MnemonicBuilder, utils::secret_key_to_address},
5};
6use alloy_primitives::Bytes;
7use commonware_codec::Encode as _;
8use commonware_cryptography::ed25519::PublicKey;
9use eyre::{WrapErr as _, eyre};
10use indicatif::{ParallelProgressIterator, ProgressIterator};
11use rayon::prelude::*;
12use reth_evm::{
13    EvmEnv, EvmFactory,
14    revm::{
15        database::{CacheDB, EmptyDB},
16        inspector::JournalExt,
17    },
18};
19use std::{
20    collections::BTreeMap,
21    net::SocketAddr,
22    path::{Path, PathBuf},
23};
24use tempo_chainspec::{hardfork::TempoHardfork, spec::TEMPO_BASE_FEE};
25use tempo_commonware_node_config::{Peers, PublicPolynomial, SigningKey, SigningShare};
26use tempo_contracts::{
27    ARACHNID_CREATE2_FACTORY_ADDRESS, CREATEX_ADDRESS, DEFAULT_7702_DELEGATE_ADDRESS,
28    MULTICALL_ADDRESS, PERMIT2_ADDRESS, SAFE_DEPLOYER_ADDRESS,
29    contracts::{ARACHNID_CREATE2_FACTORY_BYTECODE, CREATEX_POST_ALLEGRO_MODERATO_BYTECODE},
30    precompiles::{ITIP20Factory, IValidatorConfig},
31};
32use tempo_dkg_onchain_artifacts::PublicOutcome;
33use tempo_evm::evm::{TempoEvm, TempoEvmFactory};
34use tempo_precompiles::{
35    PATH_USD_ADDRESS,
36    nonce::NonceManager,
37    stablecoin_exchange::StablecoinExchange,
38    storage::{ContractStorage, StorageCtx},
39    tip_fee_manager::{IFeeManager, TipFeeManager},
40    tip20::{ISSUER_ROLE, ITIP20, TIP20Token, address_to_token_id_unchecked},
41    tip20_factory::TIP20Factory,
42    tip20_rewards_registry::TIP20RewardsRegistry,
43    tip403_registry::TIP403Registry,
44    validator_config::ValidatorConfig,
45};
46
47/// Generate genesis allocation file for testing
48#[derive(Debug, clap::Args)]
49pub(crate) struct GenesisArgs {
50    /// Number of accounts to generate
51    #[arg(short, long, default_value = "50000")]
52    accounts: u32,
53
54    /// Mnemonic to use for account generation
55    #[arg(
56        short,
57        long,
58        default_value = "test test test test test test test test test test test junk"
59    )]
60    mnemonic: String,
61
62    /// Balance for each account
63    #[arg(long, default_value = "0xD3C21BCECCEDA1000000")]
64    balance: U256,
65
66    /// Coinbase address
67    #[arg(long, default_value = "0x0000000000000000000000000000000000000000")]
68    coinbase: Address,
69
70    /// Chain ID
71    #[arg(long, short, default_value = "1337")]
72    chain_id: u64,
73
74    /// Base fee
75    #[arg(long, default_value_t = TEMPO_BASE_FEE.into())]
76    base_fee_per_gas: u128,
77
78    /// Genesis block gas limit
79    #[arg(long, default_value_t = 17000000000000)]
80    gas_limit: u64,
81
82    /// Adagio hardfork activation timestamp (defaults to 0 = active at genesis)
83    #[arg(long, default_value_t = 0)]
84    adagio_time: u64,
85
86    /// Moderato hardfork activation timestamp (defaults to 0 = active at genesis)
87    #[arg(long, default_value_t = 0)]
88    pub moderato_time: u64,
89
90    /// Allegretto hardfork activation timestamp
91    #[arg(long)]
92    pub allegretto_time: Option<u64>,
93
94    /// Allegro-Moderato hardfork activation timestamp
95    #[arg(long)]
96    pub allegro_moderato_time: Option<u64>,
97
98    /// The hard-coded length of an epoch in blocks.
99    #[arg(long, default_value_t = 302_400)]
100    epoch_length: u64,
101
102    /// A comma-separated list of `<ip>:<port>`.
103    #[arg(
104        long,
105        value_name = "<ip>:<port>",
106        value_delimiter = ',',
107        required_if_eq("allegretto_time", "0")
108    )]
109    validators: Vec<SocketAddr>,
110
111    /// Will not write the validators into the validator config contract of
112    /// the genesis block.
113    #[arg(long)]
114    no_validators_in_genesis: bool,
115
116    /// Will not write the initial DKG outcome into the extra_data field of
117    /// the genesis header.
118    #[arg(long)]
119    no_dkg_in_genesis: bool,
120
121    /// A fixed seed to generate all signing keys and group shares. This is
122    /// intended for use in development and testing. Use at your own peril.
123    #[arg(long)]
124    pub(crate) seed: Option<u64>,
125}
126
127#[derive(Clone, Debug)]
128pub(crate) struct ConsensusConfig {
129    pub(crate) public_polynomial: PublicPolynomial,
130    pub(crate) peers: Peers,
131    pub(crate) validators: Vec<Validator>,
132}
133impl ConsensusConfig {
134    pub(crate) fn to_genesis_dkg_outcome(&self) -> PublicOutcome {
135        PublicOutcome {
136            epoch: 0,
137            participants: self.peers.public_keys().clone(),
138            public: self.public_polynomial.clone().into_inner(),
139        }
140    }
141}
142
143#[derive(Clone, Debug)]
144pub(crate) struct Validator {
145    pub(crate) addr: SocketAddr,
146    pub(crate) signing_key: SigningKey,
147    pub(crate) signing_share: SigningShare,
148}
149
150impl Validator {
151    pub(crate) fn public_key(&self) -> PublicKey {
152        self.signing_key.public_key()
153    }
154
155    pub(crate) fn dst_dir(&self, path: impl AsRef<Path>) -> PathBuf {
156        path.as_ref().join(self.addr.to_string())
157    }
158    pub(crate) fn dst_signing_key(&self, path: impl AsRef<Path>) -> PathBuf {
159        self.dst_dir(path).join("signing.key")
160    }
161
162    pub(crate) fn dst_signing_share(&self, path: impl AsRef<Path>) -> PathBuf {
163        self.dst_dir(path).join("signing.share")
164    }
165}
166
167impl GenesisArgs {
168    /// Generates a genesis json file.
169    ///
170    /// It creates a new genesis allocation for the configured accounts.
171    /// And creates accounts for system contracts.
172    pub(crate) async fn generate_genesis(self) -> eyre::Result<(Genesis, Option<ConsensusConfig>)> {
173        println!("Generating {:?} accounts", self.accounts);
174
175        let addresses: Vec<Address> = (0..self.accounts)
176            .into_par_iter()
177            .progress()
178            .map(|worker_id| -> eyre::Result<Address> {
179                let signer = MnemonicBuilder::from_phrase_nth(&self.mnemonic, worker_id);
180                let address = secret_key_to_address(signer.credential());
181                Ok(address)
182            })
183            .collect::<eyre::Result<Vec<Address>>>()?;
184
185        // system contracts/precompiles must be initialized bottom up, if an init function (e.g. mint_pairwise_liquidity) uses another system contract/precompiles internally (tip403 registry), the registry must be initialized first.
186
187        // Deploy TestUSD fee token
188        let admin = addresses[0];
189        let mut evm = setup_tempo_evm();
190
191        println!("Initializing registry");
192        initialize_registry(&mut evm)?;
193
194        // Initialize TIP20Factory once before creating any tokens
195        println!("Initializing TIP20Factory");
196        initialize_tip20_factory(&mut evm)?;
197
198        // Post-Allegretto: PathUSD is created through the factory as token_id=0 with address(0) as quote token
199        println!("Creating PathUSD through factory");
200        create_path_usd_token(admin, &addresses, &mut evm)?;
201
202        println!("Initializing TIP20 tokens");
203        let (_, alpha_token_address) = create_and_mint_token(
204            "AlphaUSD",
205            "AlphaUSD",
206            "USD",
207            PATH_USD_ADDRESS,
208            admin,
209            &addresses,
210            U256::from(u64::MAX),
211            &mut evm,
212        )?;
213
214        let (_, beta_token_address) = create_and_mint_token(
215            "BetaUSD",
216            "BetaUSD",
217            "USD",
218            PATH_USD_ADDRESS,
219            admin,
220            &addresses,
221            U256::from(u64::MAX),
222            &mut evm,
223        )?;
224
225        let (_, theta_token_address) = create_and_mint_token(
226            "ThetaUSD",
227            "ThetaUSD",
228            "USD",
229            PATH_USD_ADDRESS,
230            admin,
231            &addresses,
232            U256::from(u64::MAX),
233            &mut evm,
234        )?;
235
236        println!("Initializing TIP20RewardsRegistry");
237        initialize_tip20_rewards_registry(&mut evm)?;
238
239        println!(
240            "generating consensus config for validators: {:?}",
241            self.validators
242        );
243        let consensus_config = generate_consensus_config(&self.validators, self.seed);
244
245        println!("Initializing validator config");
246        initialize_validator_config(
247            admin,
248            &mut evm,
249            &consensus_config,
250            // Skip admin
251            &addresses[1..],
252            self.no_validators_in_genesis,
253        )?;
254
255        println!("Initializing fee manager");
256        initialize_fee_manager(
257            alpha_token_address,
258            addresses.clone(),
259            // TODO: also populate validators here, once the logic is back.
260            vec![self.coinbase],
261            &mut evm,
262        );
263
264        println!("Initializing stablecoin exchange");
265        initialize_stablecoin_exchange(&mut evm)?;
266
267        println!("Initializing nonce manager");
268        initialize_nonce_manager(&mut evm)?;
269
270        println!("Minting pairwise FeeAMM liquidity");
271        mint_pairwise_liquidity(
272            alpha_token_address,
273            vec![PATH_USD_ADDRESS, beta_token_address, theta_token_address],
274            U256::from(10u64.pow(10)),
275            admin,
276            &mut evm,
277        );
278
279        // Save EVM state to allocation
280        println!("Saving EVM state to allocation");
281        let evm_state = evm.ctx_mut().journaled_state.evm_state();
282        let mut genesis_alloc: BTreeMap<Address, GenesisAccount> = evm_state
283            .iter()
284            .progress()
285            .map(|(address, account)| {
286                let storage = if !account.storage.is_empty() {
287                    Some(
288                        account
289                            .storage
290                            .iter()
291                            .map(|(key, val)| ((*key).into(), val.present_value.into()))
292                            .collect(),
293                    )
294                } else {
295                    None
296                };
297                let genesis_account = GenesisAccount {
298                    nonce: Some(account.info.nonce),
299                    code: account.info.code.as_ref().map(|c| c.original_bytes()),
300                    storage,
301                    ..Default::default()
302                };
303                (*address, genesis_account)
304            })
305            .collect();
306
307        genesis_alloc.insert(
308            MULTICALL_ADDRESS,
309            GenesisAccount {
310                code: Some(tempo_contracts::Multicall::DEPLOYED_BYTECODE.clone()),
311                nonce: Some(1),
312                ..Default::default()
313            },
314        );
315
316        genesis_alloc.insert(
317            DEFAULT_7702_DELEGATE_ADDRESS,
318            GenesisAccount {
319                code: Some(tempo_contracts::IthacaAccount::DEPLOYED_BYTECODE.clone()),
320                nonce: Some(1),
321                ..Default::default()
322            },
323        );
324
325        genesis_alloc.insert(
326            CREATEX_ADDRESS,
327            GenesisAccount {
328                code: Some(CREATEX_POST_ALLEGRO_MODERATO_BYTECODE),
329                nonce: Some(1),
330                ..Default::default()
331            },
332        );
333
334        genesis_alloc.insert(
335            SAFE_DEPLOYER_ADDRESS,
336            GenesisAccount {
337                code: Some(tempo_contracts::SafeDeployer::DEPLOYED_BYTECODE.clone()),
338                nonce: Some(1),
339                ..Default::default()
340            },
341        );
342
343        genesis_alloc.insert(
344            PERMIT2_ADDRESS,
345            GenesisAccount {
346                code: Some(tempo_contracts::Permit2::DEPLOYED_BYTECODE.clone()),
347                nonce: Some(1),
348                ..Default::default()
349            },
350        );
351
352        genesis_alloc.insert(
353            ARACHNID_CREATE2_FACTORY_ADDRESS,
354            GenesisAccount {
355                code: Some(ARACHNID_CREATE2_FACTORY_BYTECODE),
356                nonce: Some(1),
357                ..Default::default()
358            },
359        );
360
361        let mut chain_config = ChainConfig {
362            chain_id: self.chain_id,
363            homestead_block: Some(0),
364            eip150_block: Some(0),
365            eip155_block: Some(0),
366            eip158_block: Some(0),
367            byzantium_block: Some(0),
368            constantinople_block: Some(0),
369            petersburg_block: Some(0),
370            istanbul_block: Some(0),
371            berlin_block: Some(0),
372            london_block: Some(0),
373            merge_netsplit_block: Some(0),
374            shanghai_time: Some(0),
375            cancun_time: Some(0),
376            prague_time: Some(0),
377            osaka_time: Some(0),
378            terminal_total_difficulty: Some(U256::from(0)),
379            terminal_total_difficulty_passed: true,
380            deposit_contract_address: Some(address!("0x00000000219ab540356cBB839Cbe05303d7705Fa")),
381            ..Default::default()
382        };
383
384        // Add Tempo hardfork times to extra_fields
385        chain_config.extra_fields.insert(
386            "adagioTime".to_string(),
387            serde_json::json!(self.adagio_time),
388        );
389        chain_config.extra_fields.insert(
390            "moderatoTime".to_string(),
391            serde_json::json!(self.moderato_time),
392        );
393        if let Some(allegretto_time) = self.allegretto_time {
394            chain_config.extra_fields.insert(
395                "allegrettoTime".to_string(),
396                serde_json::json!(allegretto_time),
397            );
398        }
399        if let Some(allegro_moderato_time) = self.allegro_moderato_time {
400            chain_config.extra_fields.insert(
401                "allegroModeratoTime".to_string(),
402                serde_json::json!(allegro_moderato_time),
403            );
404        }
405
406        chain_config
407            .extra_fields
408            .insert_value("epochLength".to_string(), self.epoch_length)?;
409        let mut extra_data = Bytes::from_static(b"tempo-genesis");
410
411        if let Some(consensus_config) = &consensus_config {
412            chain_config
413                .extra_fields
414                .insert_value("validators".to_string(), consensus_config.peers.clone())?;
415            chain_config.extra_fields.insert_value(
416                "publicPolynomial".to_string(),
417                consensus_config.public_polynomial.clone(),
418            )?;
419
420            if self.no_dkg_in_genesis {
421                println!("no-initial-dkg-in-genesis passed; not writing to header extra_data");
422            } else {
423                extra_data = consensus_config
424                    .to_genesis_dkg_outcome()
425                    .encode()
426                    .freeze()
427                    .to_vec()
428                    .into();
429            }
430        }
431
432        let mut genesis = Genesis::default()
433            .with_gas_limit(self.gas_limit)
434            .with_base_fee(Some(self.base_fee_per_gas))
435            .with_nonce(0x42)
436            .with_extra_data(extra_data)
437            .with_coinbase(self.coinbase);
438
439        genesis.alloc = genesis_alloc;
440        genesis.config = chain_config;
441
442        Ok((genesis, consensus_config))
443    }
444}
445
446fn setup_tempo_evm() -> TempoEvm<CacheDB<EmptyDB>> {
447    let db = CacheDB::default();
448    // revm sets timestamp to 1 by default, override it to 0 for genesis initializations
449    let mut env = EvmEnv::default().with_timestamp(U256::ZERO);
450    // Configure EVM for Allegretto hardfork so factory uses correct token_id counter (starts at 0)
451    // and accepts address(0) as quote token for the first token
452    env.cfg_env = env.cfg_env.with_spec(TempoHardfork::Allegretto);
453    let factory = TempoEvmFactory::default();
454    factory.create_evm(db, env)
455}
456
457/// Initializes the TIP20Factory contract (should be called once before creating any tokens)
458fn initialize_tip20_factory(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
459    let ctx = evm.ctx_mut();
460    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
461        TIP20Factory::new().initialize()
462    })?;
463    Ok(())
464}
465
466/// Creates PathUSD as the first TIP20 token (token_id=0) through the factory.
467/// Post-Allegretto, the first token must have address(0) as quote token.
468fn create_path_usd_token(
469    admin: Address,
470    recipients: &[Address],
471    evm: &mut TempoEvm<CacheDB<EmptyDB>>,
472) -> eyre::Result<()> {
473    let ctx = evm.ctx_mut();
474    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
475        // Create PathUSD through factory with address(0) as quote token (required for first token post-Allegretto)
476        let token_address = TIP20Factory::new()
477            .create_token(
478                admin,
479                ITIP20Factory::createTokenCall {
480                    name: "pathUSD".into(),
481                    symbol: "pathUSD".into(),
482                    currency: "USD".into(),
483                    quoteToken: Address::ZERO, // First token must use address(0) as quote token
484                    admin,
485                },
486            )
487            .expect("Could not create PathUSD token");
488
489        // Verify it was created at the expected address (token_id=0)
490        assert_eq!(
491            token_address, PATH_USD_ADDRESS,
492            "PathUSD should be created at token_id=0 address"
493        );
494
495        let mut token = TIP20Token::new(0);
496        token.grant_role_internal(admin, *ISSUER_ROLE)?;
497
498        // Mint to all recipients
499        for recipient in recipients.iter().progress() {
500            token
501                .mint(
502                    admin,
503                    ITIP20::mintCall {
504                        to: *recipient,
505                        amount: U256::from(u64::MAX),
506                    },
507                )
508                .expect("Could not mint pathUSD");
509        }
510
511        Ok(())
512    })
513}
514
515/// Creates a TIP20 token through the factory (factory must already be initialized)
516#[expect(clippy::too_many_arguments)]
517fn create_and_mint_token(
518    symbol: &str,
519    name: &str,
520    currency: &str,
521    quote_token: Address,
522    admin: Address,
523    recipients: &[Address],
524    mint_amount: U256,
525    evm: &mut TempoEvm<CacheDB<EmptyDB>>,
526) -> eyre::Result<(u64, Address)> {
527    let ctx = evm.ctx_mut();
528    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
529        let mut factory = TIP20Factory::new();
530        assert!(
531            factory
532                .is_initialized()
533                .expect("Could not check factory initialization"),
534            "TIP20Factory must be initialized before creating tokens"
535        );
536        let token_address = factory
537            .create_token(
538                admin,
539                ITIP20Factory::createTokenCall {
540                    name: name.into(),
541                    symbol: symbol.into(),
542                    currency: currency.into(),
543                    quoteToken: quote_token,
544                    admin,
545                },
546            )
547            .expect("Could not create token");
548
549        let token_id = address_to_token_id_unchecked(token_address);
550
551        let mut token = TIP20Token::new(token_id);
552        token.grant_role_internal(admin, *ISSUER_ROLE)?;
553
554        let result = token.set_supply_cap(
555            admin,
556            ITIP20::setSupplyCapCall {
557                newSupplyCap: U256::from(u128::MAX),
558            },
559        );
560        assert!(result.is_ok());
561
562        token
563            .mint(
564                admin,
565                ITIP20::mintCall {
566                    to: admin,
567                    amount: mint_amount,
568                },
569            )
570            .expect("Token minting failed");
571
572        for address in recipients.iter().progress() {
573            token
574                .mint(
575                    admin,
576                    ITIP20::mintCall {
577                        to: *address,
578                        amount: U256::from(u64::MAX),
579                    },
580                )
581                .expect("Could not mint fee token");
582        }
583
584        Ok((token_id, token.address()))
585    })
586}
587
588fn initialize_tip20_rewards_registry(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
589    let ctx = evm.ctx_mut();
590    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
591        TIP20RewardsRegistry::new().initialize()
592    })?;
593
594    Ok(())
595}
596
597fn initialize_fee_manager(
598    default_fee_address: Address,
599    initial_accounts: Vec<Address>,
600    validators: Vec<Address>,
601    evm: &mut TempoEvm<CacheDB<EmptyDB>>,
602) {
603    // Update the beneficiary since the validator can't set the validator fee token for themselves
604    let ctx = evm.ctx_mut();
605    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
606        let mut fee_manager = TipFeeManager::new();
607        fee_manager
608            .initialize()
609            .expect("Could not init fee manager");
610        for address in initial_accounts.iter().progress() {
611            fee_manager
612                .set_user_token(
613                    *address,
614                    IFeeManager::setUserTokenCall {
615                        token: default_fee_address,
616                    },
617                )
618                .expect("Could not set fee token");
619        }
620
621        // Set validator fee tokens to PathUSD
622        for validator in validators {
623            fee_manager
624                .set_validator_token(
625                    validator,
626                    IFeeManager::setValidatorTokenCall {
627                        token: PATH_USD_ADDRESS,
628                    },
629                    // use random address to avoid `CannotChangeWithinBlock` error
630                    Address::random(),
631                )
632                .expect("Could not set validator fee token");
633        }
634    });
635}
636
637/// Initializes the [`TIP403Registry`] contract.
638fn initialize_registry(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
639    let ctx = evm.ctx_mut();
640    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
641        TIP403Registry::new().initialize()
642    })?;
643
644    Ok(())
645}
646
647fn initialize_stablecoin_exchange(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
648    let ctx = evm.ctx_mut();
649    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
650        StablecoinExchange::new().initialize()
651    })?;
652
653    Ok(())
654}
655
656fn initialize_nonce_manager(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
657    let ctx = evm.ctx_mut();
658    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
659        NonceManager::new().initialize()
660    })?;
661
662    Ok(())
663}
664
665/// Initializes the initial validator config smart contract.
666///
667/// NOTE: Does not populate it at all because consensus does not read the
668/// validators at genesis.
669fn initialize_validator_config(
670    admin: Address,
671    evm: &mut TempoEvm<CacheDB<EmptyDB>>,
672    consensus_config: &Option<ConsensusConfig>,
673    addresses: &[Address],
674    no_validators_in_genesis: bool,
675) -> eyre::Result<()> {
676    let ctx = evm.ctx_mut();
677    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
678        let mut validator_config = ValidatorConfig::new();
679        validator_config
680            .initialize(admin)
681            .wrap_err("failed to initialize validator config contract")?;
682
683        if no_validators_in_genesis {
684            println!("no-validators-genesis passed; not writing validators to genesis block");
685            return Ok(());
686        }
687
688        if let Some(consensus_config) = consensus_config.clone() {
689            println!(
690                "writing {} validators into contract",
691                consensus_config.validators.len()
692            );
693            for (i, validator) in consensus_config.validators.iter().enumerate() {
694                #[expect(non_snake_case, reason = "field of a snakeCase smart contract call")]
695                let newValidatorAddress = *addresses.get(i).ok_or_else(|| {
696                    eyre!(
697                        "need `{}` addresses for all validators, but only `{}` were generated",
698                        consensus_config.validators.len(),
699                        addresses.len()
700                    )
701                })?;
702                let public_key = validator.public_key();
703                let addr = validator.addr;
704                validator_config
705                    .add_validator(
706                        admin,
707                        IValidatorConfig::addValidatorCall {
708                            newValidatorAddress,
709                            publicKey: public_key.encode().freeze().as_ref().try_into().unwrap(),
710                            active: true,
711                            inboundAddress: addr.to_string(),
712                            outboundAddress: addr.to_string(),
713                        },
714                    )
715                    .wrap_err(
716                        "failed to execute smart contract call to add validator to evm state",
717                    )?;
718                println!(
719                    "added validator\
720                \n\tpublic key: {public_key}\
721                \n\tonchain address: {newValidatorAddress}\
722                \n\tnet address: {addr}"
723                );
724            }
725        } else {
726            println!("no consensus config passed; no validators to write to contract");
727        }
728
729        Ok(())
730    })
731}
732
733/// Generates the consensus configs of the validators.
734fn generate_consensus_config(
735    validators: &[SocketAddr],
736    seed: Option<u64>,
737) -> Option<ConsensusConfig> {
738    use commonware_cryptography::{PrivateKeyExt as _, Signer as _, ed25519::PrivateKey};
739    use rand::SeedableRng as _;
740
741    if validators.is_empty() {
742        println!("no validator socket addresses provided; not generating consensus config");
743        return None;
744    }
745
746    let mut rng = rand::rngs::StdRng::seed_from_u64(seed.unwrap_or_else(rand::random::<u64>));
747    let mut signers = (0..validators.len())
748        .map(|_| PrivateKey::from_rng(&mut rng))
749        .collect::<Vec<_>>();
750
751    // generate consensus key
752    let threshold = commonware_utils::quorum(validators.len() as u32);
753    let (polynomial, shares) = commonware_cryptography::bls12381::dkg::ops::generate_shares::<
754        _,
755        commonware_cryptography::bls12381::primitives::variant::MinSig,
756    >(&mut rng, None, validators.len() as u32, threshold);
757
758    signers.sort_by_key(|signer| signer.public_key());
759    let peers = validators
760        .iter()
761        .zip(signers.iter())
762        .map(|(addr, private_key)| (private_key.public_key(), *addr))
763        .collect::<commonware_utils::set::OrderedAssociated<_, _>>();
764
765    let mut validators = vec![];
766    for (addr, (signer, share)) in peers.values().iter().zip(signers.into_iter().zip(shares)) {
767        validators.push(Validator {
768            addr: *addr,
769            signing_key: SigningKey::from(signer),
770            signing_share: SigningShare::from(share),
771        });
772    }
773
774    Some(ConsensusConfig {
775        peers: peers.into(),
776        public_polynomial: polynomial.into(),
777        validators,
778    })
779}
780
781fn mint_pairwise_liquidity(
782    a_token: Address,
783    b_tokens: Vec<Address>,
784    amount: U256,
785    admin: Address,
786    evm: &mut TempoEvm<CacheDB<EmptyDB>>,
787) {
788    let ctx = evm.ctx_mut();
789    StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
790        let mut fee_manager = TipFeeManager::new();
791
792        for b_token_address in b_tokens {
793            fee_manager
794                .mint(admin, a_token, b_token_address, amount, amount, admin)
795                .expect("Could not mint A -> B Liquidity pool");
796        }
797    });
798}