cli_commands/
block_producer_metadata_signatures.rs1use 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#[derive(Clone, Debug, clap::Subcommand)]
14#[command(author, version, about, long_about = None)]
15pub enum BlockProducerMetadataSignatureCmd<AccountId: FromStrStdErr + Clone + Send + Sync + 'static>
16{
17 Upsert {
19 #[arg(long)]
21 genesis_utxo: UtxoId,
22 #[arg(long)]
24 metadata_file: String,
25 #[arg(long)]
27 cross_chain_signing_key: CrossChainSigningKeyParam,
28 #[arg(long, default_value = "3600")]
30 ttl: u64,
31 #[arg(long)]
33 partner_chain_account: AccountId,
34 },
35 Delete {
37 #[arg(long)]
39 genesis_utxo: UtxoId,
40 #[arg(long)]
42 cross_chain_signing_key: CrossChainSigningKeyParam,
43 #[arg(long, default_value = "3600")]
45 ttl: u64,
46 #[arg(long)]
49 partner_chain_account: AccountId,
50 },
51}
52
53impl<AccountId: Encode + FromStrStdErr + Clone + Send + Sync + 'static>
54 BlockProducerMetadataSignatureCmd<AccountId>
55{
56 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 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 &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}