use codec::{Decode, Encode};
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
use merkleized_metadata::{
generate_metadata_digest, generate_proof_for_extrinsic_parts, types::ExtrinsicMetadata,
ExtraInfo, FrameMetadataPrepared, Proof, SignedExtrinsicData,
};
use subtensor_macros::freeze_struct;
use crate::error::CoreError;
#[derive(Debug, Clone)]
pub struct ChainInfo {
pub spec_version: u32,
pub spec_name: String,
pub base58_prefix: u16,
pub decimals: u8,
pub token_symbol: String,
}
impl ChainInfo {
fn extra_info(&self) -> ExtraInfo {
ExtraInfo {
spec_version: self.spec_version,
spec_name: self.spec_name.clone(),
base58_prefix: self.base58_prefix,
decimals: self.decimals,
token_symbol: self.token_symbol.clone(),
}
}
}
#[freeze_struct("edb2a68250293f5")]
#[derive(Encode)]
struct ExtraInfoEncoded {
spec_version: u32,
spec_name: String,
base58_prefix: u16,
decimals: u8,
token_symbol: String,
}
impl From<ExtraInfo> for ExtraInfoEncoded {
fn from(info: ExtraInfo) -> Self {
Self {
spec_version: info.spec_version,
spec_name: info.spec_name,
base58_prefix: info.base58_prefix,
decimals: info.decimals,
token_symbol: info.token_symbol,
}
}
}
#[freeze_struct("e1038cfe3b767760")]
#[derive(Encode)]
struct MetadataProof {
proof: Proof,
extrinsic: ExtrinsicMetadata,
extra_info: ExtraInfoEncoded,
}
fn decode_metadata(metadata_bytes: &[u8]) -> Result<RuntimeMetadata, CoreError> {
let prefixed = RuntimeMetadataPrefixed::decode(&mut &metadata_bytes[..])
.map_err(|e| CoreError::Codec(format!("cannot decode runtime metadata: {e}")))?;
Ok(prefixed.1)
}
pub fn metadata_digest(metadata_bytes: &[u8], info: &ChainInfo) -> Result<[u8; 32], CoreError> {
let metadata = decode_metadata(metadata_bytes)?;
let digest =
generate_metadata_digest(&metadata, info.extra_info()).map_err(CoreError::Codec)?;
Ok(digest.hash())
}
pub fn generate_extrinsic_proof(
call_data: &[u8],
included_in_extrinsic: &[u8],
included_in_signed_data: &[u8],
metadata_bytes: &[u8],
info: &ChainInfo,
) -> Result<Vec<u8>, CoreError> {
let metadata = decode_metadata(metadata_bytes)?;
let signed_ext_data = SignedExtrinsicData {
included_in_extrinsic,
included_in_signed_data,
};
let proof = generate_proof_for_extrinsic_parts(call_data, Some(signed_ext_data), &metadata)
.map_err(CoreError::Codec)?;
let extrinsic = FrameMetadataPrepared::prepare(&metadata)
.map_err(CoreError::Codec)?
.as_type_information()
.map_err(CoreError::Codec)?
.extrinsic_metadata;
let metadata_proof = MetadataProof {
proof,
extrinsic,
extra_info: info.extra_info().into(),
};
Ok(metadata_proof.encode())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::*;
fn chain_info(spec_version: u32) -> ChainInfo {
ChainInfo {
spec_version,
spec_name: "node-subtensor".into(),
base58_prefix: 42,
decimals: 9,
token_symbol: "TAO".into(),
}
}
fn golden() -> serde_json::Value {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../python/tests/fixtures/golden.json"
);
let raw = std::fs::read_to_string(path).expect("golden.json fixture exists");
serde_json::from_str(&raw).unwrap()
}
fn golden_metadata_v15() -> Vec<u8> {
let golden = golden();
let hex_str = golden["metadata"]["v15_hex"]
.as_str()
.expect("golden.json has metadata.v15_hex");
let raw = hex::decode(hex_str.trim_start_matches("0x")).unwrap();
Option::<Vec<u8>>::decode(&mut &raw[..])
.unwrap()
.expect("fixture metadata is Some")
}
#[test]
fn digest_matches_polkadot_js_merkleize_metadata() {
let golden = golden();
let spec_version = golden["network"]["spec_version"]
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.expect("golden.json network.spec_version fits u32");
let metadata = golden_metadata_v15();
let digest = metadata_digest(&metadata, &chain_info(spec_version)).unwrap();
assert_eq!(
hex::encode(digest),
"affbcc25b1cb6847bc94dcf3a7f35fb8d6a5c59d9d47da92063a9399a5fc11c0"
);
}
#[test]
fn digest_is_deterministic_and_spec_version_sensitive() {
let metadata = golden_metadata_v15();
let digest_a = metadata_digest(&metadata, &chain_info(1)).unwrap();
let digest_b = metadata_digest(&metadata, &chain_info(1)).unwrap();
assert_eq!(digest_a, digest_b);
let digest_c = metadata_digest(&metadata, &chain_info(2)).unwrap();
assert_ne!(digest_a, digest_c);
assert_ne!(digest_a, [0u8; 32]);
}
#[test]
fn rejects_garbage_metadata() {
let result = metadata_digest(&[0u8; 16], &chain_info(1));
assert!(matches!(result, Err(CoreError::Codec(_))));
}
#[test]
fn extrinsic_proof_matches_pinned_vectors() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/fixtures/ledger_proof_vector.json"
);
let raw = std::fs::read_to_string(path).expect("ledger proof vector fixture exists");
let vector: serde_json::Value = serde_json::from_str(&raw).unwrap();
let field =
|name: &str| -> Vec<u8> { hex::decode(vector[name].as_str().unwrap()).unwrap() };
let metadata = golden_metadata_v15();
let spec_version = match &vector["spec_version"] {
serde_json::Value::Number(number) => number
.to_string()
.parse()
.expect("fixture spec_version fits u32"),
value => panic!("fixture spec_version {value} is not an integer"),
};
let proof = generate_extrinsic_proof(
&field("call_data_hex"),
&field("included_in_extrinsic_hex"),
&field("included_in_signed_data_hex"),
&metadata,
&chain_info(spec_version),
)
.unwrap();
assert_eq!(
hex::encode(&proof),
vector["rust_proof_hex"].as_str().unwrap()
);
let js_proof = field("polkadot_js_proof_hex");
let tail_len = 512.min(js_proof.len()).min(proof.len());
assert_eq!(
proof.get(proof.len() - tail_len..),
js_proof.get(js_proof.len() - tail_len..),
);
}
}