partner_chains_cli/register/
register3.rs

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}