cli_commands/
block_producer_metadata_signatures.rs

1use crate::key_params::CrossChainSigningKeyParam;
2use anyhow::anyhow;
3use byte_string::ByteString;
4use clap::Parser;
5use parity_scale_codec::Encode;
6use serde::de::DeserializeOwned;
7use serde_json::{self, json};
8use sidechain_domain::*;
9use sp_block_producer_metadata::MetadataSignedMessage;
10use std::io::{BufReader, Read};
11
12#[derive(Clone, Debug, Parser)]
13#[command(author, version, about, long_about = None)]
14pub struct BlockProducerMetadataSignatureCmd {
15	/// Genesis UTXO of the target Partner Chain
16	#[arg(long)]
17	pub genesis_utxo: UtxoId,
18	/// Path of the file containing the metadata in JSON format
19	#[arg(long)]
20	pub metadata_file: String,
21	/// ECDSA signing key of the block producer, corresponding to the public key that will be associated with new metadata
22	#[arg(long)]
23	pub cross_chain_signing_key: CrossChainSigningKeyParam,
24}
25
26impl BlockProducerMetadataSignatureCmd {
27	pub fn execute<M: Send + Sync + DeserializeOwned + Encode>(&self) -> anyhow::Result<()> {
28		let file = std::fs::File::open(self.metadata_file.clone())
29			.map_err(|err| anyhow!("Failed to open file {}: {err}", self.metadata_file))?;
30		let metadata_reader = BufReader::new(file);
31		let output = self.get_output::<M>(metadata_reader)?;
32
33		println!("{}", serde_json::to_string_pretty(&output)?);
34
35		Ok(())
36	}
37
38	pub fn get_output<M: Send + Sync + DeserializeOwned + Encode>(
39		&self,
40		metadata_reader: impl Read,
41	) -> anyhow::Result<serde_json::Value> {
42		let metadata: M = serde_json::from_reader(metadata_reader).map_err(|err| {
43			anyhow!("Failed to parse metadata: {err}. Metadata should be in JSON format.",)
44		})?;
45		let encoded_metadata = metadata.encode();
46		let message = MetadataSignedMessage {
47			cross_chain_pub_key: self.cross_chain_signing_key.vkey(),
48			metadata,
49			genesis_utxo: self.genesis_utxo,
50		};
51		let signature = message.sign_with_key(&self.cross_chain_signing_key.0);
52
53		Ok(json!({
54			"signature": signature,
55			"cross_chain_pub_key": self.cross_chain_signing_key.vkey(),
56			"cross_chain_pub_key_hash": self.cross_chain_signing_key.vkey().hash(),
57			"encoded_metadata": ByteString(encoded_metadata),
58			"encoded_message": ByteString(message.encode())
59		}))
60	}
61}
62
63#[cfg(test)]
64mod tests {
65	use super::*;
66	use crate::key_params::CrossChainSigningKeyParam;
67	use hex_literal::hex;
68	use pretty_assertions::assert_eq;
69	use serde::Deserialize;
70	use serde_json::json;
71	use sidechain_domain::UtxoId;
72
73	#[derive(Deserialize, Encode)]
74	struct TestMetadata {
75		url: String,
76		hash: String,
77	}
78
79	#[test]
80	fn produces_correct_json_output_with_signature_and_pubkey() {
81		let cmd = BlockProducerMetadataSignatureCmd {
82			genesis_utxo: UtxoId::new([1; 32], 1),
83			metadata_file: "unused".to_string(),
84			cross_chain_signing_key: CrossChainSigningKeyParam(
85				k256::SecretKey::from_slice(
86					// Alice cross-chain key
87					&hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854"),
88				)
89				.unwrap(),
90			),
91		};
92
93		let metadata_json = json!({
94			"url": "http://example.com",
95			"hash": "1234"
96		});
97		let metadata = serde_json::to_string(&metadata_json).unwrap();
98		let metadata_reader = BufReader::new(metadata.as_bytes());
99
100		let output = cmd.get_output::<TestMetadata>(metadata_reader).unwrap();
101
102		let expected_output = json!({
103			"cross_chain_pub_key": "0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
104			"cross_chain_pub_key_hash" : "0x4a20b7cab322b36838a8e4b6063c3563cdb79c97175f6c2d233dac4d",
105			"encoded_message": "0x84020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a148687474703a2f2f6578616d706c652e636f6d103132333401010101010101010101010101010101010101010101010101010101010101010100",
106			"signature": "0xf86d3aa75a6a8bda35dfdd2472b8e5f2f95446e4542ab0adb6f3e7681f01b74060082c0debfb9616a54f88cf42b88e1a2f43c75dc4394bfdde33972deb491fcb",
107			"encoded_metadata": "0x48687474703a2f2f6578616d706c652e636f6d1031323334"
108		});
109
110		assert_eq!(output, expected_output)
111	}
112
113	#[test]
114	fn retuns_error_for_invalid_json() {
115		let cmd = BlockProducerMetadataSignatureCmd {
116			genesis_utxo: UtxoId::new([1; 32], 1),
117			metadata_file: "unused".to_string(),
118			cross_chain_signing_key: CrossChainSigningKeyParam(
119				k256::SecretKey::from_slice(
120					// Alice cross-chain key
121					&hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854"),
122				)
123				.unwrap(),
124			),
125		};
126
127		let metadata_reader = BufReader::new("{ invalid json }".as_bytes());
128
129		let output = cmd.get_output::<TestMetadata>(metadata_reader);
130
131		assert!(output.is_err(), "{:?} should be Err", output);
132	}
133}