bittensor_core/digest/
mod.rs1use codec::{Decode, Encode};
18use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
19use merkleized_metadata::{
20 generate_metadata_digest, generate_proof_for_extrinsic_parts, types::ExtrinsicMetadata,
21 ExtraInfo, FrameMetadataPrepared, Proof, SignedExtrinsicData,
22};
23use subtensor_macros::freeze_struct;
24
25use crate::error::CoreError;
26
27#[derive(Debug, Clone)]
34pub struct ChainInfo {
35 pub spec_version: u32,
36 pub spec_name: String,
37 pub base58_prefix: u16,
38 pub decimals: u8,
39 pub token_symbol: String,
40}
41
42impl ChainInfo {
43 fn extra_info(&self) -> ExtraInfo {
44 ExtraInfo {
45 spec_version: self.spec_version,
46 spec_name: self.spec_name.clone(),
47 base58_prefix: self.base58_prefix,
48 decimals: self.decimals,
49 token_symbol: self.token_symbol.clone(),
50 }
51 }
52}
53
54#[freeze_struct("edb2a68250293f5")]
60#[derive(Encode)]
61struct ExtraInfoEncoded {
62 spec_version: u32,
63 spec_name: String,
64 base58_prefix: u16,
65 decimals: u8,
66 token_symbol: String,
67}
68
69impl From<ExtraInfo> for ExtraInfoEncoded {
70 fn from(info: ExtraInfo) -> Self {
71 Self {
72 spec_version: info.spec_version,
73 spec_name: info.spec_name,
74 base58_prefix: info.base58_prefix,
75 decimals: info.decimals,
76 token_symbol: info.token_symbol,
77 }
78 }
79}
80
81#[freeze_struct("e1038cfe3b767760")]
85#[derive(Encode)]
86struct MetadataProof {
87 proof: Proof,
88 extrinsic: ExtrinsicMetadata,
89 extra_info: ExtraInfoEncoded,
90}
91
92fn decode_metadata(metadata_bytes: &[u8]) -> Result<RuntimeMetadata, CoreError> {
96 let prefixed = RuntimeMetadataPrefixed::decode(&mut &metadata_bytes[..])
97 .map_err(|e| CoreError::Codec(format!("cannot decode runtime metadata: {e}")))?;
98 Ok(prefixed.1)
99}
100
101pub fn metadata_digest(metadata_bytes: &[u8], info: &ChainInfo) -> Result<[u8; 32], CoreError> {
103 let metadata = decode_metadata(metadata_bytes)?;
104 let digest =
105 generate_metadata_digest(&metadata, info.extra_info()).map_err(CoreError::Codec)?;
106 Ok(digest.hash())
107}
108
109pub fn generate_extrinsic_proof(
118 call_data: &[u8],
119 included_in_extrinsic: &[u8],
120 included_in_signed_data: &[u8],
121 metadata_bytes: &[u8],
122 info: &ChainInfo,
123) -> Result<Vec<u8>, CoreError> {
124 let metadata = decode_metadata(metadata_bytes)?;
125 let signed_ext_data = SignedExtrinsicData {
126 included_in_extrinsic,
127 included_in_signed_data,
128 };
129 let proof = generate_proof_for_extrinsic_parts(call_data, Some(signed_ext_data), &metadata)
130 .map_err(CoreError::Codec)?;
131 let extrinsic = FrameMetadataPrepared::prepare(&metadata)
132 .map_err(CoreError::Codec)?
133 .as_type_information()
134 .map_err(CoreError::Codec)?
135 .extrinsic_metadata;
136 let metadata_proof = MetadataProof {
137 proof,
138 extrinsic,
139 extra_info: info.extra_info().into(),
140 };
141 Ok(metadata_proof.encode())
142}
143
144#[cfg(test)]
145mod tests {
146 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
147
148 use super::*;
149
150 fn chain_info(spec_version: u32) -> ChainInfo {
153 ChainInfo {
154 spec_version,
155 spec_name: "node-subtensor".into(),
156 base58_prefix: 42,
157 decimals: 9,
158 token_symbol: "TAO".into(),
159 }
160 }
161
162 fn golden() -> serde_json::Value {
164 let path = concat!(
165 env!("CARGO_MANIFEST_DIR"),
166 "/../python/tests/fixtures/golden.json"
167 );
168 let raw = std::fs::read_to_string(path).expect("golden.json fixture exists");
169 serde_json::from_str(&raw).unwrap()
170 }
171
172 fn golden_metadata_v15() -> Vec<u8> {
173 let golden = golden();
174 let hex_str = golden["metadata"]["v15_hex"]
175 .as_str()
176 .expect("golden.json has metadata.v15_hex");
177 let raw = hex::decode(hex_str.trim_start_matches("0x")).unwrap();
178 Option::<Vec<u8>>::decode(&mut &raw[..])
181 .unwrap()
182 .expect("fixture metadata is Some")
183 }
184
185 #[test]
191 fn digest_matches_polkadot_js_merkleize_metadata() {
192 let golden = golden();
193 let spec_version = golden["network"]["spec_version"]
194 .as_u64()
195 .and_then(|value| u32::try_from(value).ok())
196 .expect("golden.json network.spec_version fits u32");
197 let metadata = golden_metadata_v15();
198 let digest = metadata_digest(&metadata, &chain_info(spec_version)).unwrap();
199 assert_eq!(
200 hex::encode(digest),
201 "affbcc25b1cb6847bc94dcf3a7f35fb8d6a5c59d9d47da92063a9399a5fc11c0"
202 );
203 }
204
205 #[test]
206 fn digest_is_deterministic_and_spec_version_sensitive() {
207 let metadata = golden_metadata_v15();
208 let digest_a = metadata_digest(&metadata, &chain_info(1)).unwrap();
209 let digest_b = metadata_digest(&metadata, &chain_info(1)).unwrap();
210 assert_eq!(digest_a, digest_b);
211 let digest_c = metadata_digest(&metadata, &chain_info(2)).unwrap();
214 assert_ne!(digest_a, digest_c);
215 assert_ne!(digest_a, [0u8; 32]);
216 }
217
218 #[test]
219 fn rejects_garbage_metadata() {
220 let result = metadata_digest(&[0u8; 16], &chain_info(1));
221 assert!(matches!(result, Err(CoreError::Codec(_))));
222 }
223
224 #[test]
236 fn extrinsic_proof_matches_pinned_vectors() {
237 let path = concat!(
238 env!("CARGO_MANIFEST_DIR"),
239 "/fixtures/ledger_proof_vector.json"
240 );
241 let raw = std::fs::read_to_string(path).expect("ledger proof vector fixture exists");
242 let vector: serde_json::Value = serde_json::from_str(&raw).unwrap();
243 let field =
244 |name: &str| -> Vec<u8> { hex::decode(vector[name].as_str().unwrap()).unwrap() };
245 let metadata = golden_metadata_v15();
246 let spec_version = match &vector["spec_version"] {
247 serde_json::Value::Number(number) => number
248 .to_string()
249 .parse()
250 .expect("fixture spec_version fits u32"),
251 value => panic!("fixture spec_version {value} is not an integer"),
252 };
253 let proof = generate_extrinsic_proof(
254 &field("call_data_hex"),
255 &field("included_in_extrinsic_hex"),
256 &field("included_in_signed_data_hex"),
257 &metadata,
258 &chain_info(spec_version),
259 )
260 .unwrap();
261 assert_eq!(
262 hex::encode(&proof),
263 vector["rust_proof_hex"].as_str().unwrap()
264 );
265 let js_proof = field("polkadot_js_proof_hex");
270 let tail_len = 512.min(js_proof.len()).min(proof.len());
271 assert_eq!(
272 proof.get(proof.len() - tail_len..),
273 js_proof.get(js_proof.len() - tail_len..),
274 );
275 }
276}