cli_commands/
block_producer_metadata_signatures.rs

1use crate::key_params::CrossChainSigningKeyParam;
2use anyhow::anyhow;
3use byte_string::ByteString;
4use parity_scale_codec::Encode;
5use serde::de::DeserializeOwned;
6use serde_json::{self, json};
7use sidechain_domain::*;
8use sp_block_producer_metadata::MetadataSignedMessage;
9use std::io::BufReader;
10use time_source::{SystemTimeSource, TimeSource};
11
12/// Generates ECDSA signatures for block producer metadata using cross-chain keys.
13#[derive(Clone, Debug, clap::Subcommand)]
14#[command(author, version, about, long_about = None)]
15pub enum BlockProducerMetadataSignatureCmd<AccountId: FromStrStdErr + Clone + Send + Sync + 'static>
16{
17	/// Generates signature for the `upsert_metadata` extrinsic
18	Upsert {
19		/// Genesis UTXO that uniquely identifies the target Partner Chain
20		#[arg(long)]
21		genesis_utxo: UtxoId,
22		/// Path to JSON file containing the metadata to be signed
23		#[arg(long)]
24		metadata_file: String,
25		/// ECDSA private key for cross-chain operations, corresponding to the block producer's identity
26		#[arg(long)]
27		cross_chain_signing_key: CrossChainSigningKeyParam,
28		/// Time-to-live of the signature in seconds.
29		#[arg(long, default_value = "3600")]
30		ttl: u64,
31		/// Partner Chain Account that will be used to upsert the metadata and will own it on-chain
32		#[arg(long)]
33		partner_chain_account: AccountId,
34	},
35	/// Generates signature for the `delete_metadata` extrinsic
36	Delete {
37		/// Genesis UTXO that uniquely identifies the target Partner Chain
38		#[arg(long)]
39		genesis_utxo: UtxoId,
40		/// ECDSA private key for cross-chain operations, corresponding to the block producer's identity
41		#[arg(long)]
42		cross_chain_signing_key: CrossChainSigningKeyParam,
43		/// Time-to-live of the signature in seconds.
44		#[arg(long, default_value = "3600")]
45		ttl: u64,
46		/// Partner Chain Account that will be used to delete the metadata.
47		/// It must be the account that owns it on-chain.
48		#[arg(long)]
49		partner_chain_account: AccountId,
50	},
51}
52
53impl<AccountId: Encode + FromStrStdErr + Clone + Send + Sync + 'static>
54	BlockProducerMetadataSignatureCmd<AccountId>
55{
56	/// Reads metadata file, generates signatures, and outputs JSON to stdout.
57	pub fn execute<M: Send + Sync + DeserializeOwned + Encode>(&self) -> anyhow::Result<()> {
58		let input = self.get_input::<M>()?;
59		let time_source = SystemTimeSource;
60		let output = self.get_output(input, &time_source)?;
61		println!("{}", serde_json::to_string_pretty(&output)?);
62
63		Ok(())
64	}
65
66	pub fn get_input<M: Send + Sync + DeserializeOwned + Encode>(
67		&self,
68	) -> anyhow::Result<Option<M>> {
69		Ok(match self {
70			Self::Upsert { metadata_file, .. } => {
71				let file = std::fs::File::open(metadata_file.clone())
72					.map_err(|err| anyhow!("Failed to open file {}: {err}", metadata_file))?;
73				let metadata_reader = BufReader::new(file);
74				let metadata: M = serde_json::from_reader(metadata_reader).map_err(|err| {
75					anyhow!("Failed to parse metadata: {err}. Metadata should be in JSON format.",)
76				})?;
77				Some(metadata)
78			},
79			Self::Delete { .. } => None,
80		})
81	}
82
83	/// Generates ECDSA signatures for JSON metadata from reader.
84	pub fn get_output<M: Send + Sync + DeserializeOwned + Encode>(
85		&self,
86		metadata: Option<M>,
87		time_source: &impl TimeSource,
88	) -> anyhow::Result<serde_json::Value> {
89		let encoded_metadata = metadata.as_ref().map(|data| data.encode());
90		let message = MetadataSignedMessage {
91			cross_chain_pub_key: self.cross_chain_signing_key().vkey(),
92			metadata,
93			genesis_utxo: *self.genesis_utxo(),
94			valid_before: self.valid_before(time_source),
95			owner: self.partner_chain_account(),
96		};
97		let signature = message.sign_with_key(&self.cross_chain_signing_key().0);
98
99		Ok(json!({
100			"signature": signature,
101			"cross_chain_pub_key": self.cross_chain_signing_key().vkey(),
102			"cross_chain_pub_key_hash": self.cross_chain_signing_key().vkey().hash(),
103			"encoded_metadata": encoded_metadata.map(ByteString),
104			"encoded_message": ByteString(message.encode()),
105			"valid_before": self.valid_before(time_source)
106		}))
107	}
108
109	fn cross_chain_signing_key(&self) -> &CrossChainSigningKeyParam {
110		match self {
111			Self::Delete { cross_chain_signing_key, .. } => cross_chain_signing_key,
112			Self::Upsert { cross_chain_signing_key, .. } => cross_chain_signing_key,
113		}
114	}
115
116	fn partner_chain_account(&self) -> &AccountId {
117		match self {
118			Self::Delete { partner_chain_account, .. } => partner_chain_account,
119			Self::Upsert { partner_chain_account, .. } => partner_chain_account,
120		}
121	}
122
123	fn genesis_utxo(&self) -> &UtxoId {
124		match self {
125			Self::Delete { genesis_utxo, .. } => genesis_utxo,
126			Self::Upsert { genesis_utxo, .. } => genesis_utxo,
127		}
128	}
129
130	fn valid_before(&self, time_source: &impl TimeSource) -> u64 {
131		let ttl = match self {
132			Self::Delete { ttl, .. } => *ttl,
133			Self::Upsert { ttl, .. } => *ttl,
134		};
135		let now = time_source.get_current_time_millis() / 1000;
136
137		now + ttl
138	}
139}
140
141#[cfg(test)]
142mod tests {
143	use super::*;
144	use crate::key_params::CrossChainSigningKeyParam;
145	use hex_literal::hex;
146	use pretty_assertions::assert_eq;
147	use serde::Deserialize;
148	use serde_json::json;
149	use sidechain_domain::UtxoId;
150	use time_source::MockedTimeSource;
151
152	#[derive(Deserialize, Encode)]
153	struct TestMetadata {
154		url: String,
155		hash: String,
156	}
157
158	#[test]
159	fn produces_correct_json_output_with_signature_and_pubkey() {
160		let time = 100_000_000;
161		let ttl = 3600;
162
163		let time_source = MockedTimeSource { current_time_millis: time * 1000 };
164
165		let cmd = BlockProducerMetadataSignatureCmd::Upsert {
166			genesis_utxo: UtxoId::new([1; 32], 1),
167			metadata_file: "unused".to_string(),
168			cross_chain_signing_key: CrossChainSigningKeyParam(
169				k256::SecretKey::from_slice(
170					// Alice cross-chain key
171					&hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854"),
172				)
173				.unwrap(),
174			),
175			ttl: 3600,
176			partner_chain_account: 999u32,
177		};
178
179		let metadata = TestMetadata { url: "http://example.com".into(), hash: "1234".into() };
180
181		let output = cmd.get_output::<TestMetadata>(Some(metadata), &time_source).unwrap();
182
183		let expected_output = json!({
184			"cross_chain_pub_key": "0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
185			"cross_chain_pub_key_hash" : "0x4a20b7cab322b36838a8e4b6063c3563cdb79c97175f6c2d233dac4d",
186			"encoded_message": "0x84020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a10148687474703a2f2f6578616d706c652e636f6d10313233340101010101010101010101010101010101010101010101010101010101010101010010eff50500000000e7030000",
187			"signature": "0xcfd171975a2c6ab6757c8ebbf104ba46d8b9722c17b151e4d735fa90673db1183200a6e90578f9337fe5c60d012e06f6b98be902a2d25dcf319f2f1b434bd645",
188			"encoded_metadata": "0x48687474703a2f2f6578616d706c652e636f6d1031323334",
189			"valid_before": time + ttl
190		});
191
192		assert_eq!(output, expected_output)
193	}
194}