entropy_test_cli/
lib.rs

1// Copyright (C) 2023 Entropy Cryptography Inc.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Simple CLI to test registering, updating programs and signing
17use anyhow::{anyhow, ensure};
18use clap::{Parser, Subcommand};
19use colored::Colorize;
20use entropy_client::{
21    chain_api::{
22        entropy::runtime_types::{
23            bounded_collections::bounded_vec::BoundedVec, pallet_registry::pallet::ProgramInstance,
24        },
25        EntropyConfig,
26    },
27    client::{
28        change_endpoint, change_threshold_accounts, get_accounts, get_api, get_programs, get_rpc,
29        jumpstart_network, register, remove_program, sign, store_program, update_programs,
30        VERIFYING_KEY_LENGTH,
31    },
32};
33pub use entropy_shared::PROGRAM_VERSION_NUMBER;
34use sp_core::{sr25519, Hasher, Pair};
35use sp_runtime::traits::BlakeTwo256;
36use std::{fs, path::PathBuf};
37use subxt::{
38    backend::legacy::LegacyRpcMethods,
39    utils::{AccountId32 as SubxtAccountId32, H256},
40    OnlineClient,
41};
42
43#[derive(Parser, Debug, Clone)]
44#[clap(
45    version,
46    about = "CLI tool for testing Entropy",
47    long_about = "This is a CLI test client.\nIt requires a running deployment of Entropy with at least two chain nodes and two TSS servers."
48)]
49struct Cli {
50    #[clap(subcommand)]
51    command: CliCommand,
52    /// The chain endpoint to use.
53    ///
54    /// The format should be in the form of `scheme://hostname:port`.
55    ///
56    /// Default to `ws://localhost:9944`. If a value exists for `ENTROPY_DEVNET`, that takes
57    /// priority.
58    #[arg(short, long)]
59    chain_endpoint: Option<String>,
60}
61
62#[derive(Subcommand, Debug, Clone)]
63enum CliCommand {
64    /// Register with Entropy and create keyshares
65    Register {
66        /// Either hex-encoded hashes of existing programs, or paths to wasm files to store.
67        ///
68        /// Specifying program configurations
69        ///
70        /// If there exists a file in the current directory of the same name or hex hash and
71        /// a '.json' extension, it will be read and used as the configuration for that program.
72        ///
73        /// If the path to a wasm file is given, and there is a file with the same name with a
74        /// '.interface-description' extension, it will be stored as that program's configuration
75        /// interface. If no such file exists, it is assumed the program has no configuration
76        /// interface.
77        programs: Vec<String>,
78        /// Option of version numbers to go with the programs, will default to 0 if None
79        #[arg(short, long)]
80        program_version_numbers: Option<Vec<u8>>,
81        /// A name or mnemonic from which to derive a program modification keypair.
82        /// This is used to send the register extrinsic so it must be funded
83        /// If giving a name it must be preceded with "//", eg: "--mnemonic-option //Alice"
84        /// If giving a mnemonic it must be enclosed in quotes, eg: "--mnemonic-option "alarm mutual concert...""
85        #[arg(short, long)]
86        mnemonic_option: Option<String>,
87    },
88    /// Ask the network to sign a given message
89    Sign {
90        /// The verifying key of the account to sign with, given as hex
91        signature_verifying_key: String,
92        /// The message to be signed
93        message: String,
94        /// Optional auxiliary data passed to the program, given as hex
95        auxilary_data: Option<String>,
96        /// The mnemonic to use for the call
97        #[arg(short, long)]
98        mnemonic_option: Option<String>,
99    },
100    /// Update the program for a particular account
101    UpdatePrograms {
102        /// The verifying key of the account to update their programs, given as hex
103        signature_verifying_key: String,
104        /// Either hex-encoded program hashes, or paths to wasm files to store.
105        ///
106        /// Specifying program configurations
107        ///
108        /// If there exists a file in the current directory of the same name or hex hash and
109        /// a '.json' extension, it will be read and used as the configuration for that program.
110        ///
111        /// If the path to a wasm file is given, and there is a file with the same name with a
112        /// '.interface-description' extension, it will be stored as that program's configuration
113        /// interface. If no such file exists, it is assumed the program has no configuration
114        /// interface.
115        programs: Vec<String>,
116        /// Option of version numbers to go with the programs, will default to 0 if None
117        program_version_numbers: Option<Vec<u8>>,
118        /// The mnemonic to use for the call
119        #[arg(short, long)]
120        mnemonic_option: Option<String>,
121    },
122    /// Store a given program on chain
123    StoreProgram {
124        /// The path to a .wasm file containing the program (defaults to a test program)
125        program_file: Option<PathBuf>,
126        /// The path to a file containing the program config interface (defaults to empty)
127        config_interface_file: Option<PathBuf>,
128        /// The path to a file containing the program aux interface (defaults to empty)
129        aux_data_interface_file: Option<PathBuf>,
130        /// The version number of the program's runtime you compiled with
131        program_version_number: Option<u8>,
132        /// The mnemonic to use for the call
133        #[arg(short, long)]
134        mnemonic_option: Option<String>,
135    },
136    /// Remove a given program from chain
137    RemoveProgram {
138        /// The 32 bytes hash of the program to remove, encoded as hex
139        hash: String,
140        /// The mnemonic to use for the call, which must be the program deployer
141        #[arg(short, long)]
142        mnemonic_option: Option<String>,
143    },
144    /// Allows a validator to change their endpoint
145    ChangeEndpoint {
146        /// New endpoint to change to (ex. "127.0.0.1:3001")
147        new_endpoint: String,
148        /// The mnemonic for the validator stash account to use for the call, should be stash address
149        #[arg(short, long)]
150        mnemonic_option: Option<String>,
151    },
152    /// Allows a validator to change their threshold accounts
153    ChangeThresholdAccounts {
154        /// New threshold account
155        new_tss_account: String,
156        /// New x25519 public key
157        new_x25519_public_key: String,
158        /// The mnemonic for the validator stash account to use for the call, should be stash address
159        #[arg(short, long)]
160        mnemonic_option: Option<String>,
161    },
162    /// Display a list of registered Entropy accounts
163    Status,
164    /// Triggers the network wide distributed key generation process.
165    ///
166    /// A fully jumpstarted network is required for the on-chain registration flow to work
167    /// correctly.
168    ///
169    /// Note: Any account may trigger the jumpstart process.
170    JumpstartNetwork {
171        /// The mnemonic for the signer which will trigger the jumpstart process.
172        #[arg(short, long)]
173        mnemonic_option: Option<String>,
174    },
175}
176
177pub async fn run_command(
178    program_file_option: Option<PathBuf>,
179    config_interface_file_option: Option<PathBuf>,
180    aux_data_interface_file_option: Option<PathBuf>,
181    program_version_number_option: Option<u8>,
182) -> anyhow::Result<String> {
183    let cli = Cli::parse();
184
185    let endpoint_addr = cli.chain_endpoint.unwrap_or_else(|| {
186        std::env::var("ENTROPY_DEVNET").unwrap_or("ws://localhost:9944".to_string())
187    });
188
189    let passed_mnemonic = std::env::var("DEPLOYER_MNEMONIC");
190
191    let api = get_api(&endpoint_addr).await?;
192    let rpc = get_rpc(&endpoint_addr).await?;
193
194    match cli.command {
195        CliCommand::Register { mnemonic_option, programs, program_version_numbers } => {
196            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
197                mnemonic_option
198            } else {
199                passed_mnemonic.expect("No mnemonic set")
200            };
201
202            let program_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
203            let program_account = SubxtAccountId32(program_keypair.public().0);
204            println!("Program account: {}", program_keypair.public());
205
206            let mut programs_info = vec![];
207
208            for (i, program) in programs.into_iter().enumerate() {
209                let program_version_number =
210                    program_version_numbers.as_ref().map_or(0u8, |versions| versions[i]);
211                programs_info.push(
212                    Program::from_hash_or_filename(
213                        &api,
214                        &rpc,
215                        &program_keypair,
216                        program,
217                        program_version_number,
218                    )
219                    .await?
220                    .0,
221                );
222            }
223
224            let (verifying_key, registered_info) = register(
225                &api,
226                &rpc,
227                program_keypair.clone(),
228                program_account,
229                BoundedVec(programs_info),
230            )
231            .await?;
232
233            Ok(format!("Verifying key: {},\n{:?}", hex::encode(verifying_key), registered_info))
234        },
235        CliCommand::Sign { signature_verifying_key, message, auxilary_data, mnemonic_option } => {
236            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
237                mnemonic_option
238            } else {
239                passed_mnemonic.unwrap_or("//Alice".to_string())
240            };
241            // If an account name is not provided, use the Alice key
242            let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
243
244            println!("User account for current call: {}", user_keypair.public());
245
246            let auxilary_data =
247                if let Some(data) = auxilary_data { Some(hex::decode(data)?) } else { None };
248
249            let signature_verifying_key: [u8; VERIFYING_KEY_LENGTH] =
250                hex::decode(signature_verifying_key)?
251                    .try_into()
252                    .map_err(|_| anyhow!("Verifying key must be 33 bytes"))?;
253
254            let recoverable_signature = sign(
255                &api,
256                &rpc,
257                user_keypair,
258                signature_verifying_key,
259                message.as_bytes().to_vec(),
260                auxilary_data,
261            )
262            .await?;
263            Ok(format!("Message signed: {:?}", recoverable_signature))
264        },
265        CliCommand::StoreProgram {
266            mnemonic_option,
267            program_file,
268            config_interface_file,
269            aux_data_interface_file,
270            program_version_number,
271        } => {
272            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
273                mnemonic_option
274            } else {
275                passed_mnemonic.expect("No Mnemonic set")
276            };
277            let keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
278            println!("Storing program using account: {}", keypair.public());
279
280            let program = match program_file {
281                Some(file_name) => fs::read(file_name)?,
282                None => fs::read(program_file_option.expect("No program file passed in"))?,
283            };
284
285            let config_interface = match config_interface_file {
286                Some(file_name) => fs::read(file_name)?,
287                None => fs::read(
288                    config_interface_file_option.expect("No config interface file passed"),
289                )?,
290            };
291
292            let aux_data_interface = match aux_data_interface_file {
293                Some(file_name) => fs::read(file_name)?,
294                None => fs::read(
295                    aux_data_interface_file_option.expect("No aux data interface file passed"),
296                )?,
297            };
298
299            let program_version_number = match program_version_number_option {
300                Some(program_version_number) => program_version_number,
301                None => program_version_number.unwrap_or(0u8),
302            };
303
304            let hash = store_program(
305                &api,
306                &rpc,
307                &keypair,
308                program,
309                config_interface,
310                aux_data_interface,
311                vec![],
312                program_version_number,
313            )
314            .await?;
315            Ok(format!("Program stored: {}", hex::encode(hash)))
316        },
317        CliCommand::RemoveProgram { mnemonic_option, hash } => {
318            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
319                mnemonic_option
320            } else {
321                passed_mnemonic.expect("No Mnemonic set")
322            };
323            let keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
324            println!("Removing program using account: {}", keypair.public());
325
326            let hash: [u8; 32] = hex::decode(hash)?
327                .try_into()
328                .map_err(|_| anyhow!("Program hash must be 32 bytes"))?;
329
330            remove_program(&api, &rpc, &keypair, H256(hash)).await?;
331
332            Ok("Program removed".to_string())
333        },
334        CliCommand::UpdatePrograms {
335            signature_verifying_key,
336            mnemonic_option,
337            programs,
338            program_version_numbers,
339        } => {
340            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
341                mnemonic_option
342            } else {
343                passed_mnemonic.expect("No Mnemonic set")
344            };
345            let program_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
346            println!("Program account: {}", program_keypair.public());
347
348            let mut programs_info = Vec::new();
349
350            for (i, program) in programs.into_iter().enumerate() {
351                let program_version_number =
352                    program_version_numbers.as_ref().map_or(0u8, |versions| versions[i]);
353                programs_info.push(
354                    Program::from_hash_or_filename(
355                        &api,
356                        &rpc,
357                        &program_keypair,
358                        program,
359                        program_version_number,
360                    )
361                    .await?
362                    .0,
363                );
364            }
365
366            let verifying_key: [u8; VERIFYING_KEY_LENGTH] = hex::decode(signature_verifying_key)?
367                .try_into()
368                .map_err(|_| anyhow!("Verifying key must be 33 bytes"))?;
369
370            update_programs(&api, &rpc, verifying_key, &program_keypair, BoundedVec(programs_info))
371                .await?;
372
373            Ok("Programs updated".to_string())
374        },
375        CliCommand::Status => {
376            let accounts = get_accounts(&api, &rpc).await?;
377            println!(
378                "There are {} registered Entropy accounts.\n",
379                accounts.len().to_string().green()
380            );
381            if !accounts.is_empty() {
382                println!(
383                    "{:<64} {:<12} Programs:",
384                    "Verifying key:".green(),
385                    "Visibility:".purple(),
386                );
387                for (account_id, info) in accounts {
388                    println!(
389                        "{} {}",
390                        hex::encode(account_id).green(),
391                        format!(
392                            "{:?}",
393                            info.programs_data
394                                .0
395                                .iter()
396                                .map(|program_instance| format!(
397                                    "{}",
398                                    program_instance.program_pointer
399                                ))
400                                .collect::<Vec<_>>()
401                        )
402                        .white(),
403                    );
404                }
405            }
406
407            let programs = get_programs(&api, &rpc).await?;
408
409            println!("\nThere are {} stored programs\n", programs.len().to_string().green());
410
411            if !programs.is_empty() {
412                println!(
413                    "{:<64} {:<48} {:<11} {:<14} {} {}",
414                    "Hash".blue(),
415                    "Stored by:".green(),
416                    "Times used:".purple(),
417                    "Size in bytes:".cyan(),
418                    "Configurable?".yellow(),
419                    "Has auxiliary?".yellow(),
420                );
421                for (hash, program_info) in programs {
422                    println!(
423                        "{} {} {:>11} {:>14} {:<13} {}",
424                        hex::encode(hash),
425                        program_info.deployer,
426                        program_info.ref_counter,
427                        program_info.bytecode.len(),
428                        !program_info.configuration_schema.is_empty(),
429                        !program_info.auxiliary_data_schema.is_empty(),
430                    );
431                }
432            }
433
434            Ok("Got status".to_string())
435        },
436        CliCommand::ChangeEndpoint { new_endpoint, mnemonic_option } => {
437            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
438                mnemonic_option
439            } else {
440                passed_mnemonic.expect("No Mnemonic set")
441            };
442
443            let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
444            println!("User account for current call: {}", user_keypair.public());
445
446            let result_event = change_endpoint(&api, &rpc, user_keypair, new_endpoint).await?;
447            println!("Event result: {:?}", result_event);
448            Ok("Endpoint changed".to_string())
449        },
450        CliCommand::ChangeThresholdAccounts {
451            new_tss_account,
452            new_x25519_public_key,
453            mnemonic_option,
454        } => {
455            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
456                mnemonic_option
457            } else {
458                passed_mnemonic.expect("No Mnemonic set")
459            };
460            let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
461            println!("User account for current call: {}", user_keypair.public());
462
463            let result_event = change_threshold_accounts(
464                &api,
465                &rpc,
466                user_keypair,
467                new_tss_account,
468                new_x25519_public_key,
469            )
470            .await?;
471            println!("Event result: {:?}", result_event);
472
473            Ok("Threshold accounts changed".to_string())
474        },
475        CliCommand::JumpstartNetwork { mnemonic_option } => {
476            let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
477                mnemonic_option
478            } else {
479                passed_mnemonic.unwrap_or("//Alice".to_string())
480            };
481
482            let signer = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
483            println!("Account being used for jumpstart: {}", signer.public());
484
485            jumpstart_network(&api, &rpc, signer).await?;
486
487            Ok("Succesfully jumpstarted network.".to_string())
488        },
489    }
490}
491
492struct Program(ProgramInstance);
493
494impl Program {
495    fn new(program_pointer: H256, program_config: Vec<u8>) -> Self {
496        Self(ProgramInstance { program_pointer, program_config })
497    }
498
499    async fn from_hash_or_filename(
500        api: &OnlineClient<EntropyConfig>,
501        rpc: &LegacyRpcMethods<EntropyConfig>,
502        keypair: &sr25519::Pair,
503        hash_or_filename: String,
504        program_version_number: u8,
505    ) -> anyhow::Result<Self> {
506        match hex::decode(hash_or_filename.clone()) {
507            Ok(hash) => {
508                let hash_32_res: Result<[u8; 32], _> = hash.try_into();
509                match hash_32_res {
510                    Ok(hash_32) => {
511                        // If there is a file called <hash as hex>.json use that as the
512                        // configuration:
513                        let configuration = {
514                            let mut configuration_file = PathBuf::from(&hash_or_filename);
515                            configuration_file.set_extension("json");
516                            fs::read(&configuration_file).unwrap_or_default()
517                        };
518                        Ok(Self::new(H256(hash_32), configuration))
519                    },
520                    Err(_) => {
521                        Self::from_file(api, rpc, keypair, hash_or_filename, program_version_number)
522                            .await
523                    },
524                }
525            },
526            Err(_) => {
527                Self::from_file(api, rpc, keypair, hash_or_filename, program_version_number).await
528            },
529        }
530    }
531
532    /// Given a path to a .wasm file, read it, store the program if it doesn't already exist, and
533    /// return the hash.
534    async fn from_file(
535        api: &OnlineClient<EntropyConfig>,
536        rpc: &LegacyRpcMethods<EntropyConfig>,
537        keypair: &sr25519::Pair,
538        filename: String,
539        program_version_number: u8,
540    ) -> anyhow::Result<Self> {
541        let program_bytecode = fs::read(&filename)?;
542
543        // If there is a file with the same name with the '.config-description' extension, read it
544        let config_description = {
545            let mut config_description_file = PathBuf::from(&filename);
546            config_description_file.set_extension("config-description");
547            fs::read(&config_description_file).unwrap_or_default()
548        };
549
550        // If there is a file with the same name with the '.aux-description' extension, read it
551        let auxiliary_data_schema = {
552            let mut auxiliary_data_schema_file = PathBuf::from(&filename);
553            auxiliary_data_schema_file.set_extension("aux-description");
554            fs::read(&auxiliary_data_schema_file).unwrap_or_default()
555        };
556
557        // If there is a file with the same name with the '.json' extension, read it
558        let configuration = {
559            let mut configuration_file = PathBuf::from(&filename);
560            configuration_file.set_extension("json");
561            fs::read(&configuration_file).unwrap_or_default()
562        };
563
564        ensure!(
565            (config_description.is_empty() && configuration.is_empty())
566                || (!config_description.is_empty() && !configuration.is_empty()),
567            "If giving an interface description you must also give a configuration"
568        );
569
570        match store_program(
571            api,
572            rpc,
573            keypair,
574            program_bytecode.clone(),
575            config_description,
576            auxiliary_data_schema,
577            vec![],
578            program_version_number,
579        )
580        .await
581        {
582            Ok(hash) => Ok(Self::new(hash, configuration)),
583            Err(error) => {
584                if error.to_string().ends_with("ProgramAlreadySet") {
585                    println!("Program is already stored - using existing one");
586                    let hash = BlakeTwo256::hash(&program_bytecode);
587                    Ok(Self::new(H256(hash.into()), configuration))
588                } else {
589                    Err(error.into())
590                }
591            },
592        }
593    }
594}