Skip to main content

bittensor_core/runtime/
mod.rs

1//! `Runtime`: one immutable object per spec version, parsed from the raw
2//! `MetadataVersioned` bytes the transport already downloads and caches.
3//!
4//! Every metadata question the SDK asks — storage entries, call composition,
5//! constants, module errors, runtime APIs, the codegen IR — is answered here,
6//! keyed by portable-registry type *ids* internally and by *names* at the
7//! seam (ids are not stable across spec versions, so they never leave the
8//! process; see the spec's §4.2).
9//!
10//! Immutable and `Send + Sync`: decoded values are materialized fresh per
11//! call and nothing mutates after `parse`.
12
13// Client-side metadata views, not runtime code.
14#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
15
16pub mod type_string;
17
18use std::collections::HashMap;
19
20use codec::Decode;
21use frame_metadata::{v14, v15, RuntimeMetadata, RuntimeMetadataPrefixed};
22use scale_info::{form::PortableForm, PortableRegistry, TypeDef};
23
24use crate::error::CoreError;
25
26/// One storage item's full description: everything needed to build keys for
27/// and decode values of it.
28#[derive(Debug, Clone)]
29pub struct StorageInfo {
30    pub name: String,
31    /// The pallet's storage prefix string (usually the pallet name).
32    pub prefix: String,
33    /// "Default" | "Optional"
34    pub modifier: String,
35    pub hashers: Vec<String>,
36    /// Unhashed key component type ids (one per hasher).
37    pub key_types: Vec<u32>,
38    pub value_type: u32,
39    pub default_bytes: Vec<u8>,
40}
41
42#[derive(Debug, Clone)]
43pub struct ConstantInfo {
44    pub name: String,
45    pub ty: u32,
46    pub value: Vec<u8>,
47    pub docs: Vec<String>,
48}
49
50#[derive(Debug, Clone)]
51pub struct PalletInfo {
52    pub name: String,
53    pub index: u8,
54    /// The pallet's `Call` enum type id (variant type), when it has calls.
55    pub calls_type: Option<u32>,
56    /// The pallet's `Event` enum type id, when it has events.
57    pub events_type: Option<u32>,
58    /// The pallet's `Error` enum type id, when it has errors.
59    pub errors_type: Option<u32>,
60    pub constants: Vec<ConstantInfo>,
61    pub storage: Vec<StorageInfo>,
62}
63
64#[derive(Debug, Clone)]
65pub struct SignedExtensionInfo {
66    pub identifier: String,
67    /// Type id of the bytes that travel in the extrinsic ("extra").
68    pub ty: u32,
69    /// Type id of the implied bytes both sides sign ("additional_signed").
70    pub additional_signed: u32,
71}
72
73#[derive(Debug, Clone)]
74pub struct ExtrinsicInfo {
75    pub version: u8,
76    pub address_type: Option<u32>,
77    pub call_type: Option<u32>,
78    pub signature_type: Option<u32>,
79    pub signed_extensions: Vec<SignedExtensionInfo>,
80}
81
82#[derive(Debug, Clone)]
83pub struct RuntimeApiParamInfo {
84    pub name: String,
85    pub ty: u32,
86}
87
88#[derive(Debug, Clone)]
89pub struct RuntimeApiMethodInfo {
90    pub name: String,
91    pub inputs: Vec<RuntimeApiParamInfo>,
92    pub output: u32,
93    pub docs: Vec<String>,
94}
95
96#[derive(Debug, Clone)]
97pub struct RuntimeApiInfo {
98    pub name: String,
99    pub methods: Vec<RuntimeApiMethodInfo>,
100}
101
102/// V14 and V15 pallet metadata share the fields we read but are distinct
103/// types; the macro is the shared conversion.
104macro_rules! pallet_info {
105    ($pallet:expr, $types:expr) => {{
106        let pallet = $pallet;
107        let storage = match &pallet.storage {
108            Some(s) => s
109                .entries
110                .iter()
111                .map(|e| storage_info(&s.prefix, e, $types))
112                .collect(),
113            None => Vec::new(),
114        };
115        PalletInfo {
116            name: pallet.name.clone(),
117            index: pallet.index,
118            calls_type: pallet.calls.as_ref().map(|c| c.ty.id),
119            events_type: pallet.event.as_ref().map(|e| e.ty.id),
120            errors_type: pallet.error.as_ref().map(|e| e.ty.id),
121            constants: pallet
122                .constants
123                .iter()
124                .map(|c| ConstantInfo {
125                    name: c.name.clone(),
126                    ty: c.ty.id,
127                    value: c.value.clone(),
128                    docs: c.docs.clone(),
129                })
130                .collect(),
131            storage,
132        }
133    }};
134}
135
136/// One runtime's complete metadata view.
137pub struct Runtime {
138    pub spec_version: u32,
139    pub transaction_version: u32,
140    pub ss58_format: u16,
141    pub is_v15: bool,
142    /// The raw `MetadataVersioned` blob this was parsed from (fed to the
143    /// RFC-0078 digest without re-downloading).
144    pub metadata_bytes: Vec<u8>,
145    pub types: PortableRegistry,
146    pub pallets: Vec<PalletInfo>,
147    pub extrinsic: ExtrinsicInfo,
148    pub apis: Vec<RuntimeApiInfo>,
149    /// The outer RuntimeEvent enum type id (drives the flattened event shape).
150    pub outer_event_type: Option<u32>,
151    pallet_by_name: HashMap<String, usize>,
152    pallet_by_index: HashMap<u8, usize>,
153    /// Registry type-name maps (the `_name_maps` logic from the Python codec):
154    /// unambiguous last-path-segment names plus derived Vec<...>/(...) names.
155    name_to_id: HashMap<String, u32>,
156    id_to_name: HashMap<u32, String>,
157}
158
159impl Runtime {
160    /// Parse a raw `MetadataVersioned` blob (magic `meta` + version byte +
161    /// V14/V15 payload — what `state_getMetadata` / `Metadata_metadata_at_version`
162    /// return, unwrapped).
163    pub fn parse(
164        metadata_bytes: &[u8],
165        spec_version: u32,
166        transaction_version: u32,
167        ss58_format: u16,
168    ) -> Result<Self, CoreError> {
169        let prefixed = RuntimeMetadataPrefixed::decode(&mut &metadata_bytes[..])
170            .map_err(|e| CoreError::Codec(format!("cannot decode runtime metadata: {e}")))?;
171        let (types, pallets, extrinsic, apis, outer_event_type, is_v15) = match prefixed.1 {
172            RuntimeMetadata::V14(m) => {
173                let pallets = m
174                    .pallets
175                    .iter()
176                    .map(|p| pallet_info!(p, &m.types))
177                    .collect();
178                let extrinsic = extrinsic_info_v14(&m.extrinsic, &m.types);
179                let outer_event = find_outer_event_v14(&m.types);
180                (m.types, pallets, extrinsic, Vec::new(), outer_event, false)
181            }
182            RuntimeMetadata::V15(m) => {
183                let pallets = m
184                    .pallets
185                    .iter()
186                    .map(|p| pallet_info!(p, &m.types))
187                    .collect();
188                let extrinsic = extrinsic_info_v15(&m.extrinsic);
189                let apis = m.apis.iter().map(api_info_v15).collect();
190                let outer_event = Some(m.outer_enums.event_enum_ty.id);
191                (m.types, pallets, extrinsic, apis, outer_event, true)
192            }
193            other => {
194                return Err(CoreError::Codec(format!(
195                    "unsupported metadata version V{}",
196                    other.version()
197                )))
198            }
199        };
200        let pallets: Vec<PalletInfo> = pallets;
201        let pallet_by_name = pallets
202            .iter()
203            .enumerate()
204            .map(|(i, p)| (p.name.clone(), i))
205            .collect();
206        let pallet_by_index = pallets
207            .iter()
208            .enumerate()
209            .map(|(i, p)| (p.index, i))
210            .collect();
211        let (name_to_id, id_to_name) = build_name_maps(&types);
212        Ok(Self {
213            spec_version,
214            transaction_version,
215            ss58_format,
216            is_v15,
217            metadata_bytes: metadata_bytes.to_vec(),
218            types,
219            pallets,
220            extrinsic,
221            apis,
222            outer_event_type,
223            pallet_by_name,
224            pallet_by_index,
225            name_to_id,
226            id_to_name,
227        })
228    }
229
230    pub fn pallet(&self, name: &str) -> Option<&PalletInfo> {
231        self.pallet_by_name.get(name).map(|&i| &self.pallets[i])
232    }
233
234    pub fn pallet_at(&self, index: u8) -> Option<&PalletInfo> {
235        self.pallet_by_index.get(&index).map(|&i| &self.pallets[i])
236    }
237
238    pub fn storage_entry(&self, pallet: &str, name: &str) -> Option<&StorageInfo> {
239        self.pallet(pallet)?.storage.iter().find(|s| s.name == name)
240    }
241
242    pub fn constant(&self, pallet: &str, name: &str) -> Option<&ConstantInfo> {
243        self.pallet(pallet)?
244            .constants
245            .iter()
246            .find(|c| c.name == name)
247    }
248
249    pub fn type_id_of(&self, name: &str) -> Option<u32> {
250        self.name_to_id.get(name).copied()
251    }
252
253    pub fn type_name_of(&self, id: u32) -> Option<&str> {
254        self.id_to_name.get(&id).map(String::as_str)
255    }
256
257    pub fn resolve(&self, id: u32) -> Result<&scale_info::Type<PortableForm>, CoreError> {
258        self.types
259            .resolve(id)
260            .ok_or_else(|| CoreError::Codec(format!("unknown type id {id}")))
261    }
262
263    /// A short, human-facing identity for a registry type: the last segment
264    /// of its path (e.g. "TaoBalance", "NetUid", "AccountId32"), with
265    /// `Option`'s payload kept (`Option<Timepoint>`). Path-less types render
266    /// structurally: "u64", "Vec<u16>", "[u8; 32]", "(u16, u16)",
267    /// "Compact<u64>".
268    ///
269    /// This is what the codegen IR carries per call parameter, so generated
270    /// builders can surface newtype identity (a TaoBalance argument vs a
271    /// bare u64) even though the wire encoding is the inner value.
272    pub fn type_ident(&self, id: u32) -> String {
273        self.type_ident_bounded(id, 0)
274    }
275
276    fn type_ident_bounded(&self, id: u32, depth: usize) -> String {
277        // The registry comes from the connected node and is untrusted; bound
278        // recursion like the decoders do.
279        if depth > 8 {
280            return format!("scale_info::{id}");
281        }
282        let Ok(ty) = self.resolve(id) else {
283            return format!("scale_info::{id}");
284        };
285        if let Some(last) = ty.path.segments.last() {
286            if last == "Option" && ty.path.segments.len() == 1 {
287                if let TypeDef::Variant(variant) = &ty.type_def {
288                    if let Some(field) = variant
289                        .variants
290                        .iter()
291                        .find(|v| v.name == "Some")
292                        .and_then(|v| v.fields.first())
293                    {
294                        return format!(
295                            "Option<{}>",
296                            self.type_ident_bounded(field.ty.id, depth + 1)
297                        );
298                    }
299                }
300            }
301            return last.clone();
302        }
303        match &ty.type_def {
304            TypeDef::Primitive(p) => primitive_name(p).to_string(),
305            TypeDef::Compact(c) => format!(
306                "Compact<{}>",
307                self.type_ident_bounded(c.type_param.id, depth + 1)
308            ),
309            TypeDef::Sequence(s) => format!(
310                "Vec<{}>",
311                self.type_ident_bounded(s.type_param.id, depth + 1)
312            ),
313            TypeDef::Array(a) => format!(
314                "[{}; {}]",
315                self.type_ident_bounded(a.type_param.id, depth + 1),
316                a.len
317            ),
318            TypeDef::Tuple(t) => {
319                let parts: Vec<String> = t
320                    .fields
321                    .iter()
322                    .map(|f| self.type_ident_bounded(f.id, depth + 1))
323                    .collect();
324                format!("({})", parts.join(", "))
325            }
326            _ => format!("scale_info::{id}"),
327        }
328    }
329
330    /// The portable registry as JSON (`{"types": [{"id", "type": {...}}]}`).
331    ///
332    /// For registry-walking tooling (the shape-corpus recorder); not a hot
333    /// path.
334    pub fn registry_json(&self) -> Result<String, CoreError> {
335        serde_json::to_string(&self.types)
336            .map_err(|e| CoreError::Codec(format!("registry serialization failed: {e}")))
337    }
338
339    /// `{"type": "Module", "name", "docs"}` inputs for a dispatch module error.
340    pub fn module_error(
341        &self,
342        module_index: u8,
343        error_index: u8,
344    ) -> Result<(String, Vec<String>), CoreError> {
345        let pallet = self
346            .pallet_at(module_index)
347            .ok_or_else(|| CoreError::Codec(format!("no pallet at index {module_index}")))?;
348        let errors_type = pallet
349            .errors_type
350            .ok_or_else(|| CoreError::Codec(format!("pallet {} has no errors", pallet.name)))?;
351        let ty = self.resolve(errors_type)?;
352        let TypeDef::Variant(variant) = &ty.type_def else {
353            return Err(CoreError::Codec("error type is not an enum".into()));
354        };
355        let error = variant
356            .variants
357            .iter()
358            .find(|v| v.index == error_index)
359            .ok_or_else(|| {
360                CoreError::Codec(format!(
361                    "no error at index {error_index} in pallet {}",
362                    pallet.name
363                ))
364            })?;
365        Ok((error.name.clone(), error.docs.clone()))
366    }
367}
368
369fn storage_info(
370    prefix: &str,
371    entry: &v14::StorageEntryMetadata<PortableForm>,
372    types: &PortableRegistry,
373) -> StorageInfo {
374    let modifier = match entry.modifier {
375        v14::StorageEntryModifier::Optional => "Optional",
376        v14::StorageEntryModifier::Default => "Default",
377    };
378    let (hashers, key_types, value_type) = match &entry.ty {
379        v14::StorageEntryType::Plain(ty) => (Vec::new(), Vec::new(), ty.id),
380        v14::StorageEntryType::Map {
381            hashers,
382            key,
383            value,
384        } => {
385            let hasher_names: Vec<String> = hashers.iter().map(|h| format!("{h:?}")).collect();
386            // One hasher, one key type; several hashers hash the components
387            // of a key tuple separately.
388            let key_ids: Vec<u32> = if hasher_names.len() == 1 {
389                vec![key.id]
390            } else {
391                match types.resolve(key.id).map(|t| &t.type_def) {
392                    Some(TypeDef::Tuple(tuple)) => tuple.fields.iter().map(|f| f.id).collect(),
393                    _ => vec![key.id],
394                }
395            };
396            (hasher_names, key_ids, value.id)
397        }
398    };
399    StorageInfo {
400        name: entry.name.clone(),
401        prefix: prefix.to_string(),
402        modifier: modifier.to_string(),
403        hashers,
404        key_types,
405        value_type,
406        default_bytes: entry.default.clone(),
407    }
408}
409
410fn extrinsic_info_v14(
411    extrinsic: &v14::ExtrinsicMetadata<PortableForm>,
412    types: &PortableRegistry,
413) -> ExtrinsicInfo {
414    // V14 carries only the UncheckedExtrinsic type; its generic params name
415    // the Address / Call / Signature / Extra types.
416    let mut address_type = None;
417    let mut call_type = None;
418    let mut signature_type = None;
419    if let Some(ty) = types.resolve(extrinsic.ty.id) {
420        for param in &ty.type_params {
421            let target = match param.name.as_str() {
422                "Address" => &mut address_type,
423                "Call" => &mut call_type,
424                "Signature" => &mut signature_type,
425                _ => continue,
426            };
427            *target = param.ty.map(|t| t.id);
428        }
429    }
430    ExtrinsicInfo {
431        version: extrinsic.version,
432        address_type,
433        call_type,
434        signature_type,
435        signed_extensions: extrinsic
436            .signed_extensions
437            .iter()
438            .map(|e| SignedExtensionInfo {
439                identifier: e.identifier.clone(),
440                ty: e.ty.id,
441                additional_signed: e.additional_signed.id,
442            })
443            .collect(),
444    }
445}
446
447fn extrinsic_info_v15(extrinsic: &v15::ExtrinsicMetadata<PortableForm>) -> ExtrinsicInfo {
448    ExtrinsicInfo {
449        version: extrinsic.version,
450        address_type: Some(extrinsic.address_ty.id),
451        call_type: Some(extrinsic.call_ty.id),
452        signature_type: Some(extrinsic.signature_ty.id),
453        signed_extensions: extrinsic
454            .signed_extensions
455            .iter()
456            .map(|e| SignedExtensionInfo {
457                identifier: e.identifier.clone(),
458                ty: e.ty.id,
459                additional_signed: e.additional_signed.id,
460            })
461            .collect(),
462    }
463}
464
465/// V14 has no outer_enums section; the outer event enum is the `event` field
466/// of `frame_system::EventRecord`.
467fn find_outer_event_v14(types: &PortableRegistry) -> Option<u32> {
468    let record = types.types.iter().find(|t| {
469        t.ty.path.segments.len() == 2
470            && t.ty.path.segments[0] == "frame_system"
471            && t.ty.path.segments[1] == "EventRecord"
472    })?;
473    let TypeDef::Composite(composite) = &record.ty.type_def else {
474        return None;
475    };
476    composite
477        .fields
478        .iter()
479        .find(|f| f.name.as_deref() == Some("event"))
480        .map(|f| f.ty.id)
481}
482
483fn api_info_v15(api: &v15::RuntimeApiMetadata<PortableForm>) -> RuntimeApiInfo {
484    RuntimeApiInfo {
485        name: api.name.clone(),
486        methods: api
487            .methods
488            .iter()
489            .map(|m| RuntimeApiMethodInfo {
490                name: m.name.clone(),
491                inputs: m
492                    .inputs
493                    .iter()
494                    .map(|p| RuntimeApiParamInfo {
495                        name: p.name.clone(),
496                        ty: p.ty.id,
497                    })
498                    .collect(),
499                output: m.output.id,
500                docs: m.docs.clone(),
501            })
502            .collect(),
503    }
504}
505
506/// The registry name maps: unambiguous last-path-segment names for concrete
507/// types, plus `Vec<...>` / `[T; N]` / `Compact<...>` / tuple names derived
508/// bottom-up — the same fixed-point the Python `_name_maps` computed. Used by
509/// the legacy runtime-call registry, which addresses types by name.
510fn build_name_maps(types: &PortableRegistry) -> (HashMap<String, u32>, HashMap<u32, String>) {
511    let mut name_to_id: HashMap<String, u32> = HashMap::new();
512    let mut id_to_name: HashMap<u32, String> = HashMap::new();
513
514    for ty in types.types.iter() {
515        let def = &ty.ty.type_def;
516        if !ty.ty.type_params.is_empty() || matches!(def, TypeDef::Variant(_)) {
517            continue;
518        }
519        if let Some(segment) = ty.ty.path.segments.last() {
520            name_to_id.insert(segment.clone(), ty.id);
521            id_to_name.insert(ty.id, segment.clone());
522        } else if let TypeDef::Primitive(p) = def {
523            let name = primitive_name(p);
524            name_to_id.insert(name.to_string(), ty.id);
525            id_to_name.insert(ty.id, name.to_string());
526        }
527    }
528
529    let mut progressed = true;
530    while progressed {
531        progressed = false;
532        for ty in types.types.iter() {
533            if id_to_name.contains_key(&ty.id) {
534                continue;
535            }
536            let resolved: Option<String> = (|| {
537                let def = &ty.ty.type_def;
538                if let Some(segment) = ty.ty.path.segments.last() {
539                    if !ty.ty.type_params.is_empty() {
540                        let mut inner = Vec::new();
541                        for param in &ty.ty.type_params {
542                            let dep = param.ty?.id;
543                            inner.push(id_to_name.get(&dep)?.clone());
544                        }
545                        return Some(format!("{segment}<{}>", inner.join(", ")));
546                    }
547                    if matches!(def, TypeDef::Variant(_)) {
548                        return None;
549                    }
550                    return Some(segment.clone());
551                }
552                match def {
553                    TypeDef::Sequence(s) => {
554                        Some(format!("Vec<{}>", id_to_name.get(&s.type_param.id)?))
555                    }
556                    TypeDef::Array(a) => Some(format!(
557                        "[{}; {}]",
558                        id_to_name.get(&a.type_param.id)?,
559                        a.len
560                    )),
561                    TypeDef::Compact(c) => {
562                        Some(format!("Compact<{}>", id_to_name.get(&c.type_param.id)?))
563                    }
564                    TypeDef::Tuple(t) => {
565                        let mut names = Vec::new();
566                        for field in &t.fields {
567                            names.push(id_to_name.get(&field.id)?.clone());
568                        }
569                        Some(format!("({})", names.join(", ")))
570                    }
571                    _ => None,
572                }
573            })();
574            if let Some(name) = resolved {
575                name_to_id.insert(name.clone(), ty.id);
576                id_to_name.insert(ty.id, name);
577                progressed = true;
578            }
579        }
580    }
581    (name_to_id, id_to_name)
582}
583
584pub(crate) fn primitive_name(p: &scale_info::TypeDefPrimitive) -> &'static str {
585    use scale_info::TypeDefPrimitive as P;
586    match p {
587        P::Bool => "bool",
588        P::Char => "char",
589        P::Str => "str",
590        P::U8 => "u8",
591        P::U16 => "u16",
592        P::U32 => "u32",
593        P::U64 => "u64",
594        P::U128 => "u128",
595        P::U256 => "u256",
596        P::I8 => "i8",
597        P::I16 => "i16",
598        P::I32 => "i32",
599        P::I64 => "i64",
600        P::I128 => "i128",
601        P::I256 => "i256",
602    }
603}