1use crate::CmdRun;
2use crate::cardano_key::get_mc_payment_signing_key_from_file;
3use crate::config;
4use crate::config::CHAIN_CONFIG_FILE_PATH;
5use crate::config::config_fields;
6use crate::data_source::set_data_sources_env;
7use crate::io::IOContext;
8use crate::ogmios::config::establish_ogmios_configuration;
9use crate::register::Register;
10use clap::Parser;
11use sidechain_domain::mainchain_epoch::{MainchainEpochConfig, MainchainEpochDerivation};
12use sidechain_domain::*;
13use sidechain_domain::{CandidateRegistration, UtxoId};
14
15#[derive(Clone, Debug, Parser)]
16#[command(author, version, about, long_about = None)]
17pub struct Register3Cmd {
18 #[clap(flatten)]
19 common_arguments: crate::CommonArguments,
20 #[arg(long)]
21 pub genesis_utxo: UtxoId,
22 #[arg(long)]
23 pub registration_utxo: UtxoId,
24 #[arg(long)]
25 pub partner_chain_pub_key: SidechainPublicKey,
26 #[arg(long)]
27 pub aura_pub_key: AuraPublicKey,
28 #[arg(long)]
29 pub grandpa_pub_key: GrandpaPublicKey,
30 #[arg(long)]
31 pub partner_chain_signature: SidechainSignature,
32 #[arg(long)]
33 pub spo_public_key: StakePoolPublicKey,
34 #[arg(long)]
35 pub spo_signature: MainchainSignature,
36}
37
38impl CmdRun for Register3Cmd {
39 fn run<C: IOContext>(&self, context: &C) -> anyhow::Result<()> {
40 context.print("⚙️ Register as a committee candidate (step 3/3)");
41 context.print("This command will submit the registration message to the mainchain.");
42
43 config_fields::GENESIS_UTXO.load_from_file(context).ok_or_else(|| {
44 context.eprint(&format!("⚠️ The chain configuration file `{CHAIN_CONFIG_FILE_PATH}` is missing or invalid.\n"));
45 context.eprint("⚠️ If you are the governance authority, please make sure you have run the `prepare-configuration` command to generate the chain configuration file.\n");
46 context.eprint("⚠️ If you are a validator, you can obtain the chain configuration file from the governance authority.\n");
47 anyhow::anyhow!("Chain config missing or invalid")
48 })?;
49
50 context.print("To proceed with the next command, a payment signing key is required. Please note that this key will not be stored or communicated over the network.");
51
52 let cardano_payment_signing_key_path = config_fields::CARDANO_PAYMENT_SIGNING_KEY_FILE
53 .prompt_with_default_from_file_and_save(context);
54
55 let payment_signing_key =
56 get_mc_payment_signing_key_from_file(&cardano_payment_signing_key_path, context)?;
57 let ogmios_configuration = establish_ogmios_configuration(context)?;
58 let candidate_registration = CandidateRegistration {
59 stake_ownership: AdaBasedStaking {
60 pub_key: self.spo_public_key.clone(),
61 signature: self.spo_signature.clone(),
62 },
63 partner_chain_pub_key: self.partner_chain_pub_key.clone(),
64 partner_chain_signature: self.partner_chain_signature.clone(),
65 own_pkh: payment_signing_key.to_pub_key_hash(),
66 registration_utxo: self.registration_utxo,
67 aura_pub_key: self.aura_pub_key.clone(),
68 grandpa_pub_key: self.grandpa_pub_key.clone(),
69 };
70 let offchain = context.offchain_impl(&ogmios_configuration)?;
71
72 let runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
73 runtime
74 .block_on(offchain.register(
75 self.common_arguments.retries(),
76 self.genesis_utxo,
77 &candidate_registration,
78 &payment_signing_key,
79 ))
80 .map_err(|e| anyhow::anyhow!("Candidate registration failed: {e:?}!"))?;
81
82 if context.prompt_yes_no("Show registration status?", true) {
83 context.print("The registration status will be queried from a db-sync instance for which a valid connection string is required. Please note that this db-sync instance needs to be up and synced with the main chain.");
84 let current_mc_epoch_number = get_current_mainchain_epoch(context).map_err(|e| {
85 context.eprint(format!("Unable to derive current mainchain epoch: {}", e).as_str());
86 anyhow::anyhow!("{}", e)
87 })?;
88 let mc_epoch_number_to_query = current_mc_epoch_number.next().next();
89 prepare_mc_follower_env(context)?;
90 context.print(&format!("Registrations status for epoch {mc_epoch_number_to_query}:"));
91 show_registration_status(
92 context,
93 mc_epoch_number_to_query,
94 self.spo_public_key.clone(),
95 )?;
96 }
97
98 Ok(())
99 }
100}
101
102fn prepare_mc_follower_env<C: IOContext>(context: &C) -> anyhow::Result<()> {
103 let postgres_connection_string =
104 config_fields::POSTGRES_CONNECTION_STRING.prompt_with_default_from_file_and_save(context);
105 let chain_config = config::load_chain_config(context)?;
106 set_data_sources_env(context, &chain_config.cardano, &postgres_connection_string);
107 Ok(())
108}
109
110fn show_registration_status(
111 context: &impl IOContext,
112 mc_epoch_number: McEpochNumber,
113 stake_pool_public_key: StakePoolPublicKey,
114) -> Result<(), anyhow::Error> {
115 let temp_dir = context.new_tmp_dir();
116 let temp_dir_path = temp_dir
117 .into_os_string()
118 .into_string()
119 .expect("PathBuf is a valid UTF-8 String");
120 let node_executable = context.current_executable()?;
121 let command = format!(
122 "{} registration-status --mainchain-pub-key {} --mc-epoch-number {} --chain chain-spec.json --base-path {temp_dir_path}",
123 node_executable,
124 stake_pool_public_key.to_hex_string(),
125 mc_epoch_number
126 );
127 let output = context.run_command(&command)?;
128 context.print("Registration status:");
129 context.print(&output);
130 Ok(())
131}
132
133#[derive(serde::Deserialize, Debug, Clone)]
134struct McEpochConfigJson {
135 cardano: MainchainEpochConfig,
136}
137
138fn get_current_mainchain_epoch(context: &impl IOContext) -> Result<McEpochNumber, anyhow::Error> {
139 let chain_config_json = context.read_file(config::CHAIN_CONFIG_FILE_PATH).ok_or_else(|| {
140 anyhow::anyhow!(
141 "⚠️ The chain configuration file `{CHAIN_CONFIG_FILE_PATH}` is missing or invalid."
142 )
143 })?;
144
145 let mc_epoch_config = serde_json::from_str::<McEpochConfigJson>(&chain_config_json)?;
146 mc_epoch_config
147 .cardano
148 .timestamp_to_mainchain_epoch(context.current_timestamp())
149 .map_err(|e| anyhow::anyhow!("{}", e))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::{
156 config::{
157 CHAIN_CONFIG_FILE_PATH, RESOURCES_CONFIG_FILE_PATH,
158 config_fields::POSTGRES_CONNECTION_STRING,
159 },
160 ogmios::config::tests::{
161 default_ogmios_config_json, default_ogmios_service_config,
162 establish_ogmios_configuration_io,
163 },
164 tests::{MockIO, MockIOContext, OffchainMock, OffchainMocks},
165 verify_json,
166 };
167 use hex_literal::hex;
168 use serde_json::json;
169 use sp_core::offchain::Timestamp;
170
171 #[test]
172 fn happy_path() {
173 let offchain_mock = OffchainMock::new().with_register(
174 genesis_utxo(),
175 new_candidate_registration(),
176 payment_signing_key(),
177 Ok(Some(McTxHash::default())),
178 );
179
180 let mock_context = MockIOContext::new()
181 .with_json_file("/path/to/payment.skey", payment_skey_content())
182 .with_json_file(CHAIN_CONFIG_FILE_PATH, chain_config_content())
183 .with_json_file(RESOURCES_CONFIG_FILE_PATH, resource_config_content())
184 .with_offchain_mocks(OffchainMocks::new_with_mock(
185 "http://localhost:1337",
186 offchain_mock,
187 ))
188 .with_expected_io(
189 vec![
190 intro_msg_io(),
191 prompt_mc_payment_key_path_io(),
192 get_ogmios_config(),
193 prompt_for_registration_status_y(),
194 show_registration_status_io(),
195 ]
196 .into_iter()
197 .flatten()
198 .collect::<Vec<MockIO>>(),
199 );
200
201 let result = mock_register3_cmd().run(&mock_context);
202 result.expect("should succeed");
203 verify_json!(mock_context, RESOURCES_CONFIG_FILE_PATH, final_resources_config_json());
204 }
205
206 #[test]
207 fn registration_call_fails() {
208 let offchain_mock = OffchainMock::new().with_register(
209 genesis_utxo(),
210 new_candidate_registration(),
211 payment_signing_key(),
212 Err("test error".to_string()),
213 );
214 let mock_context = MockIOContext::new()
215 .with_json_file("/path/to/payment.skey", payment_skey_content())
216 .with_json_file(CHAIN_CONFIG_FILE_PATH, chain_config_content())
217 .with_json_file(RESOURCES_CONFIG_FILE_PATH, resource_config_content())
218 .with_offchain_mocks(OffchainMocks::new_with_mock(
219 "http://localhost:1337",
220 offchain_mock,
221 ))
222 .with_expected_io(
223 vec![intro_msg_io(), prompt_mc_payment_key_path_io(), get_ogmios_config()]
224 .into_iter()
225 .flatten()
226 .collect::<Vec<MockIO>>(),
227 );
228
229 let result = mock_register3_cmd().run(&mock_context);
230 result.expect_err("should return error");
231 }
232
233 #[test]
234 fn not_show_registration_status() {
235 let offchain_mock = OffchainMock::new().with_register(
236 genesis_utxo(),
237 new_candidate_registration(),
238 payment_signing_key(),
239 Ok(Some(McTxHash::default())),
240 );
241 let mock_context = MockIOContext::new()
242 .with_json_file("/path/to/payment.skey", payment_skey_content())
243 .with_json_file(CHAIN_CONFIG_FILE_PATH, chain_config_content())
244 .with_json_file(RESOURCES_CONFIG_FILE_PATH, resource_config_content())
245 .with_offchain_mocks(OffchainMocks::new_with_mock(
246 "http://localhost:1337",
247 offchain_mock,
248 ))
249 .with_expected_io(
250 vec![
251 intro_msg_io(),
252 prompt_mc_payment_key_path_io(),
253 get_ogmios_config(),
254 prompt_for_registration_status_n(),
255 ]
256 .into_iter()
257 .flatten()
258 .collect::<Vec<MockIO>>(),
259 );
260
261 let result = mock_register3_cmd().run(&mock_context);
262 result.expect("should succeed");
263 }
264
265 fn intro_msg_io() -> Vec<MockIO> {
266 vec![
267 MockIO::print("⚙️ Register as a committee candidate (step 3/3)"),
268 MockIO::print("This command will submit the registration message to the mainchain."),
269 MockIO::print(
270 "To proceed with the next command, a payment signing key is required. Please note that this key will not be stored or communicated over the network.",
271 ),
272 ]
273 }
274
275 fn prompt_mc_payment_key_path_io() -> Vec<MockIO> {
276 vec![MockIO::prompt(
277 "path to the payment signing key file",
278 Some("payment.skey"),
279 "/path/to/payment.skey",
280 )]
281 }
282
283 fn get_ogmios_config() -> Vec<MockIO> {
284 vec![establish_ogmios_configuration_io(
285 Some(default_ogmios_service_config()),
286 default_ogmios_service_config(),
287 )]
288 }
289
290 fn prompt_for_registration_status_y() -> Vec<MockIO> {
291 vec![MockIO::prompt_yes_no("Show registration status?", true, true)]
292 }
293 fn prompt_for_registration_status_n() -> Vec<MockIO> {
294 vec![MockIO::prompt_yes_no("Show registration status?", true, false)]
295 }
296
297 fn show_registration_status_io() -> Vec<MockIO> {
298 vec![
299 MockIO::print(
300 "The registration status will be queried from a db-sync instance for which a valid connection string is required. Please note that this db-sync instance needs to be up and synced with the main chain.",
301 ),
302 MockIO::current_timestamp(mock_timestamp()),
303 MockIO::prompt(
304 "DB-Sync Postgres connection string",
305 POSTGRES_CONNECTION_STRING.default,
306 POSTGRES_CONNECTION_STRING.default.unwrap(),
307 ),
308 MockIO::set_env_var(
309 "DB_SYNC_POSTGRES_CONNECTION_STRING",
310 POSTGRES_CONNECTION_STRING.default.unwrap(),
311 ),
312 MockIO::set_env_var("CARDANO_SECURITY_PARAMETER", "1234"),
313 MockIO::set_env_var("CARDANO_ACTIVE_SLOTS_COEFF", "0.1"),
314 MockIO::set_env_var("BLOCK_STABILITY_MARGIN", "0"),
315 MockIO::set_env_var("MC__FIRST_EPOCH_TIMESTAMP_MILLIS", "1666742400000"),
316 MockIO::set_env_var("MC__FIRST_EPOCH_NUMBER", "1"),
317 MockIO::set_env_var("MC__EPOCH_DURATION_MILLIS", "86400000"),
318 MockIO::set_env_var("MC__FIRST_SLOT_NUMBER", "4320"),
319 MockIO::print("Registrations status for epoch 25:"),
320 MockIO::new_tmp_dir(),
321 MockIO::run_command(
322 "<mock executable> registration-status --mainchain-pub-key 0xcef2d1630c034d3b9034eb7903d61f419a3074a1ad01d4550cc72f2b733de6e7 --mc-epoch-number 25 --chain chain-spec.json --base-path /tmp/MockIOContext_tmp_dir",
323 "{\"epoch\":1,\"validators\":[{\"public_key\":\"cef2d1630c034d3b9034eb7903d61f419a3074a1ad01d4550cc72f2b733de6e7\",\"status\":\"Registered\"}]}",
324 ),
325 MockIO::print("Registration status:"),
326 MockIO::print(
327 "{\"epoch\":1,\"validators\":[{\"public_key\":\"cef2d1630c034d3b9034eb7903d61f419a3074a1ad01d4550cc72f2b733de6e7\",\"status\":\"Registered\"}]}",
328 ),
329 ]
330 }
331
332 fn mock_register3_cmd() -> Register3Cmd {
333 Register3Cmd {
334 common_arguments: crate::CommonArguments { retry_delay_seconds: 5, retry_count: 59 },
335 genesis_utxo: genesis_utxo(),
336 registration_utxo: "cdefe62b0a0016c2ccf8124d7dda71f6865283667850cc7b471f761d2bc1eb13#0".parse().unwrap(),
337 partner_chain_pub_key: "020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1".parse().unwrap(),
338 aura_pub_key: "79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf".parse().unwrap(),
339 grandpa_pub_key: "1a55db596380bc63f5ee964565359b5ea8e0096c798c3281692df097abbd9aa4b657f887915ad2a52fc85c674ef4044baeaf7149546af93a2744c379b9798f07".parse().unwrap(),
340 partner_chain_signature: SidechainSignature(hex_literal::hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854").to_vec()),
341 spo_public_key: StakePoolPublicKey(hex_literal::hex!("cef2d1630c034d3b9034eb7903d61f419a3074a1ad01d4550cc72f2b733de6e7")),
342 spo_signature: MainchainSignature(hex_literal::hex!("aaa39fbf163ed77c69820536f5dc22854e7e13f964f1e077efde0844a09bde64c1aab4d2b401e0fe39b43c91aa931cad26fa55c8766378462c06d86c85134801")),
343 }
344 }
345
346 fn genesis_utxo() -> UtxoId {
347 "f17e6d3aa72095e04489d13d776bf05a66b5a8c49d89397c28b18a1784b9950e#0"
348 .parse()
349 .unwrap()
350 }
351
352 fn payment_skey_content() -> serde_json::Value {
353 serde_json::json!({
354 "type": "PaymentSigningKeyShelley_ed25519",
355 "description": "Payment Signing Key",
356 "cborHex": "5820d75c630516c33a66b11b3444a70b65083aeb21353bd919cc5e3daa02c9732a84"
357 })
358 }
359
360 fn payment_signing_key() -> Vec<u8> {
361 hex!("d75c630516c33a66b11b3444a70b65083aeb21353bd919cc5e3daa02c9732a84").to_vec()
362 }
363
364 fn chain_config_content() -> serde_json::Value {
365 json!({
366 "chain_parameters": chain_parameters_json(),
367 "cardano": {
368 "security_parameter": 1234,
369 "active_slots_coeff": 0.1,
370 "first_epoch_timestamp_millis": 1_666_742_400_000_i64,
371 "epoch_duration_millis": 86400000,
372 "first_epoch_number": 1,
373 "first_slot_number": 4320,
374 "slot_duration_millis": 1000,
375 "network": "mainnet"
376 },
377 "cardano_addresses": {
378 "committee_candidates_address": "addr_test1wz5qc7fk2pat0058w4zwvkw35ytptej3nuc3je2kgtan5dq3rt4sc",
379 "d_parameter_policy_id": "d0ebb61e2ba362255a7c4a253c6578884603b56fb0a68642657602d6",
380 "permissioned_candidates_policy_id": "58b4ba68f641d58f7f1bba07182eca9386da1e88a34d47a14638c3fe",
381 "native_token": {
382 "asset": {
383 "policy_id": "ada83ddd029614381f00e28de0922ab0dec6983ea9dd29ae20eef9b4",
384 "asset_name": "5043546f6b656e44656d6f",
385 },
386 "illiquid_supply_address": "addr_test1wqn2pkvvmesmxtfa4tz7w8gh8vumr52lpkrhcs4dkg30uqq77h5z4"
387 },
388 },
389 "initial_permissioned_candidates": [
390 {
391 "aura_pub_key": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
392 "grandpa_pub_key": "0x88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee",
393 "partner_chain_pub_key": "0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1"
394 },
395 {
396 "aura_pub_key": "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",
397 "grandpa_pub_key": "0xd17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae69",
398 "partner_chain_pub_key": "0x0390084fdbf27d2b79d26a4f13f0ccd982cb755a661969143c37cbc49ef5b91f27"
399 }
400 ],
401 })
402 }
403
404 fn final_resources_config_json() -> serde_json::Value {
405 json!({
406 "cardano_payment_signing_key_file": "/path/to/payment.skey",
407 "cardano_payment_verification_key_file": "payment.vkey",
408 "db_sync_postgres_connection_string": "postgresql://postgres-user:postgres-password@localhost:5432/cexplorer",
409 "ogmios": default_ogmios_config_json(),
410 "substrate_node_base_path": "/path/to/data",
411 "substrate_node_executable_path": "/path/to/node"
412 })
413 }
414
415 fn chain_parameters_json() -> serde_json::Value {
416 json!({
417 "genesis_utxo": "0000000000000000000000000000000000000000000000000000000000000000#0"
418 })
419 }
420
421 fn resource_config_content() -> serde_json::Value {
422 serde_json::json!({
423 "substrate_node_base_path": "/path/to/data",
424 "substrate_node_executable_path": "/path/to/node",
425 "cardano_payment_verification_key_file": "payment.vkey",
426 })
427 }
428
429 fn mock_timestamp() -> Timestamp {
430 Timestamp::from_unix_millis(1668658000000u64)
431 }
432
433 fn new_candidate_registration() -> CandidateRegistration {
434 CandidateRegistration {
435 stake_ownership: AdaBasedStaking {
436 pub_key: StakePoolPublicKey(hex!("cef2d1630c034d3b9034eb7903d61f419a3074a1ad01d4550cc72f2b733de6e7")),
437 signature: MainchainSignature(hex!("aaa39fbf163ed77c69820536f5dc22854e7e13f964f1e077efde0844a09bde64c1aab4d2b401e0fe39b43c91aa931cad26fa55c8766378462c06d86c85134801"))
438 },
439 partner_chain_pub_key: SidechainPublicKey(hex!("020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1").to_vec()),
440 partner_chain_signature: SidechainSignature(hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854").to_vec()),
441 own_pkh: MainchainKeyHash(hex!("7fa48bb8fb5d6804fad26237738ce490d849e4567161e38ab8415ff3")),
442 registration_utxo: UtxoId { tx_hash: McTxHash(hex!("cdefe62b0a0016c2ccf8124d7dda71f6865283667850cc7b471f761d2bc1eb13")), index: UtxoIndex(0) },
443 aura_pub_key: AuraPublicKey(hex!("79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf").to_vec()),
444 grandpa_pub_key: GrandpaPublicKey(hex!("1a55db596380bc63f5ee964565359b5ea8e0096c798c3281692df097abbd9aa4b657f887915ad2a52fc85c674ef4044baeaf7149546af93a2744c379b9798f07").to_vec())
445 }
446 }
447}