Skip to main content

hoprd_localcluster/
identity.rs

1use std::path::PathBuf;
2
3use anyhow::Context;
4use hopr_chain_connector::{
5    BlockchainConnectorConfig,
6    api::*,
7    blokli_client::{BlokliClient, BlokliClientConfig, BlokliQueryClient},
8    create_trustful_safeless_hopr_blokli_connector,
9    reexports::chain::exports::alloy::hex,
10};
11use hopr_lib::{ChainKeypair, HoprKeys, Keypair, SafeModule, XDaiBalance, crypto_traits::Randomizable};
12use hopr_reference::config::SessionIpForwardingConfig;
13use hoprd::config::{Db, HoprdConfig, Identity, UserHoprLibConfig, UserHoprNetworkConfig};
14use hoprd_api::config::{Api, Auth};
15
16pub const DEFAULT_BLOKLI_URL: &str = "http://localhost:8080";
17pub const DEFAULT_PRIVATE_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
18pub const DEFAULT_CONFIG_HOME: &str = "/tmp/hopr-nodes";
19pub const DEFAULT_IDENTITY_PASSWORD: &str = "password";
20pub const DEFAULT_NUM_NODES: usize = 3;
21// Increased tx client timeout multiplier for Anvil
22pub const DEFAULT_TX_TIMEOUT_MULTIPLIER: u32 = 10;
23
24#[derive(Clone, Debug)]
25pub struct GenerationConfig {
26    pub blokli_url: String,
27    pub private_key: String,
28    pub num_nodes: usize,
29    pub config_home: PathBuf,
30    pub identity_password: String,
31    pub random_identities: bool,
32}
33
34impl Default for GenerationConfig {
35    fn default() -> Self {
36        Self {
37            blokli_url: DEFAULT_BLOKLI_URL.to_string(),
38            private_key: DEFAULT_PRIVATE_KEY.to_string(),
39            num_nodes: DEFAULT_NUM_NODES,
40            config_home: PathBuf::from(DEFAULT_CONFIG_HOME),
41            identity_password: DEFAULT_IDENTITY_PASSWORD.to_string(),
42            random_identities: false,
43        }
44    }
45}
46
47lazy_static::lazy_static! {
48    static ref NODE_KEYS: [HoprKeys; 5] = [
49        (
50            hex!("76a4edbc3f595d4d07671779a0055e30b2b8477ecfd5d23c37afd7b5aa83781d"),
51            hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a")
52        ).try_into().unwrap(),
53        (
54            hex!("c90f09e849aa512be3dd007452977e32c7cfdc1e3de1a62bd92ba6592bcc9e90"),
55            hex!("c3659450e994f3ad086373440e4e7070629a1bfbd555387237ccb28d17acbfc8")
56        ).try_into().unwrap(),
57        (
58            hex!("40d4749a620d1a4278d030a3153b5b94d6fcd4f9677f6ce8e37e6ebb1987ad53"),
59            hex!("4a14c5aeb53629a2dd45058a8d233f24dd90192189e8200a1e5f10069868f963")
60        ).try_into().unwrap(),
61        (
62            hex!("e539f1ac48270be4e84b6acfe35252df5e141a29b50ddb07b50670271bb574ee"),
63            hex!("8c1edcdebfe508031e4124168bb4a133180e8ee68207a7946fcdc4ad0068ef0d")
64        ).try_into().unwrap(),
65        (
66            hex!("9ab557eb14d8b081c7e1750eb87407d8c421aa79bdeb420f38980829e7dbf936"),
67            hex!("6075c595103667537c33cdb954e3e5189921cab942e5fc0ba9ec27fe6d7787d1")
68        ).try_into().unwrap()
69    ];
70}
71
72/// Generate test node Safes and hoprd configuration files.
73///
74/// This generates node identities, deploys/funds Safes, and writes hoprd config files.
75pub async fn generate(config: &GenerationConfig) -> anyhow::Result<()> {
76    std::fs::create_dir_all(&config.config_home)?;
77    let home_path = &config.config_home;
78    let private_key = hex::decode(&config.private_key).context("invalid private key")?;
79
80    let blokli_client = BlokliClient::new(config.blokli_url.parse()?, BlokliClientConfig::default());
81    let status = blokli_client.query_health().await?;
82    if !status.eq_ignore_ascii_case("ok") {
83        return Err(anyhow::anyhow!("Blokli is not usable: {status}"));
84    }
85
86    // Create connector for the deployer account
87    let mut anvil_connector = create_trustful_safeless_hopr_blokli_connector(
88        &ChainKeypair::from_secret(&private_key)?,
89        BlockchainConnectorConfig {
90            tx_timeout_multiplier: DEFAULT_TX_TIMEOUT_MULTIPLIER,
91            ..Default::default()
92        },
93        blokli_client.clone(),
94    )
95    .await?;
96    anvil_connector.connect().await?;
97
98    let initial_token_balance: HoprBalance = "1000 wxHOPR".parse()?;
99    let initial_native_balance: XDaiBalance = "1 xDai".parse()?;
100
101    for id in 0..config.num_nodes.clamp(1, NODE_KEYS.len()) {
102        let kp = if config.random_identities {
103            HoprKeys::random()
104        } else {
105            NODE_KEYS[id].clone()
106        };
107        let node_address = kp.chain_key.public().to_address();
108        eprintln!("Node {id}: Address {node_address}");
109
110        let node_connector = std::sync::Arc::new(
111            create_trustful_safeless_hopr_blokli_connector(
112                &kp.chain_key,
113                BlockchainConnectorConfig {
114                    tx_timeout_multiplier: DEFAULT_TX_TIMEOUT_MULTIPLIER,
115                    ..Default::default()
116                },
117                blokli_client.clone(),
118            )
119            .await?,
120        );
121
122        eprint!("Node {id}: Checking balances...");
123
124        // Send 1 xDai to the new node address from Anvil 0 account
125        let node_native_balance: XDaiBalance = node_connector.balance(node_address).await?;
126        if node_native_balance < initial_native_balance {
127            let top_up = initial_native_balance - node_native_balance;
128            if anvil_connector.balance(*anvil_connector.me()).await? < top_up {
129                return Err(anyhow::anyhow!(
130                    "Account {} must have at least {top_up}.",
131                    anvil_connector.me()
132                ));
133            }
134
135            anvil_connector.withdraw(top_up, &node_address).await?.await?;
136            eprint!("\x1b[2K\rNode {id}: {top_up} transferred to {node_address}");
137        } else {
138            eprint!("\x1b[2K\rNode {id}: {node_address} already has {node_native_balance} xDai tokens");
139        }
140
141        eprint!("\x1b[2K\rNode {id}: Checking Safe deployment...");
142        let safe = if let Some(safe) = node_connector.safe_info(SafeSelector::Owner(node_address)).await? {
143            safe
144        } else {
145            // Send 1000 wxHOPR tokens to the new node address from Anvil 0 account
146            eprint!("\x1b[2K\rNode {id}: Topping up to {initial_token_balance}...");
147            let node_token_balance: HoprBalance = node_connector.balance(node_address).await?;
148            if node_token_balance < initial_token_balance {
149                let top_up = initial_token_balance - node_token_balance;
150                if anvil_connector.balance(*anvil_connector.me()).await? < top_up {
151                    return Err(anyhow::anyhow!(
152                        "Account {} must have at least {top_up}.",
153                        anvil_connector.me()
154                    ));
155                }
156
157                anvil_connector.withdraw(top_up, &node_address).await?.await?;
158                eprint!("\x1b[2K\rNode {id}: {top_up} transferred to {node_address}");
159            } else {
160                eprint!("\x1b[2K\rNode {id}: {node_address} already has {node_token_balance} wxHOPR tokens");
161            }
162
163            eprint!("\x1b[2K\rNode {id}: Deploying Safe...");
164            let node_connector_clone = node_connector.clone();
165            let jh = tokio::task::spawn(async move {
166                node_connector_clone
167                    .await_safe_deployment(SafeSelector::Owner(node_address), std::time::Duration::from_secs(10))
168                    .await
169            });
170            node_connector.deploy_safe(initial_token_balance).await?.await?;
171            jh.await??
172        };
173
174        let id_file = home_path
175            .join(format!("node_id_{id}.id"))
176            .to_str()
177            .ok_or(anyhow::anyhow!("Invalid path"))?
178            .to_owned();
179
180        let node_cfg = HoprdConfig {
181            hopr: UserHoprLibConfig {
182                announce: true,
183                network: UserHoprNetworkConfig {
184                    announce_local_addresses: true,
185                    prefer_local_addresses: true,
186                    ..Default::default()
187                },
188                safe_module: SafeModule {
189                    safe_address: safe.address,
190                    module_address: safe.module,
191                },
192                ..Default::default()
193            },
194            identity: Identity {
195                file: id_file.clone(),
196                password: config.identity_password.clone(),
197                private_key: None,
198            },
199            db: Db {
200                data: home_path
201                    .join(format!("db_{id}"))
202                    .to_str()
203                    .ok_or(anyhow::anyhow!("Invalid path"))?
204                    .to_owned(),
205                initialize: true,
206                force_initialize: true,
207            },
208            api: Api {
209                enable: true,
210                auth: Auth::None,
211                ..Default::default()
212            },
213            blokli_url: Some(config.blokli_url.clone()),
214            session_ip_forwarding: SessionIpForwardingConfig {
215                use_target_allow_list: false,
216                ..Default::default()
217            },
218            ..Default::default()
219        };
220
221        let cfg_file = home_path
222            .join(format!("hoprd_cfg_{id}.yaml"))
223            .to_str()
224            .ok_or(anyhow::anyhow!("Invalid path"))?
225            .to_owned();
226        std::fs::write(&cfg_file, serde_saphyr::to_string(&node_cfg)?)?;
227        kp.write_eth_keystore(&id_file, &config.identity_password)?;
228
229        eprintln!("\x1b[2K\rNode {id}: Node config written to {cfg_file}");
230    }
231
232    Ok(())
233}