Skip to main content

bittensor_core/codec/
decode.rs

1//! SCALE decode into cyscale's value shapes (see `value.rs`).
2//!
3//! Every convention here is pinned by the shape corpus
4//! (`sdk/python/tests/fixtures/shape_corpus`): AccountId32 renders as ss58,
5//! `Vec<u8>` as utf8-else-hex strings, `[u8; N]` as 0x-hex, enums as
6//! `{"Variant": payload}` (unit variants as bare strings), Options unwrap,
7//! newtype composites unwrap, BTreeMaps become dicts with native keys, and
8//! `Era` renders as `"00"` / `(period, phase)`.
9
10// Client-side codec, not runtime code: slices are guarded by the cursor and
11// explicit length checks; arithmetic operates on lengths and era math.
12#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
13
14use scale_info::{form::PortableForm, Field, TypeDef, TypeDefPrimitive, Variant};
15
16use crate::codec::value::Value;
17use crate::error::CoreError;
18use crate::keys::ss58_from_public;
19use crate::runtime::type_string::{Primitive, TypeSpec};
20use crate::runtime::Runtime;
21
22/// Recursion ceiling for the decoders. Metadata comes from the connected
23/// node and is untrusted: a self-referential type recurses without consuming
24/// bytes, and a stack overflow is an abort (not a catchable panic) — fatal on
25/// a rayon worker with no Python frame above it. Real chain values nest a
26/// couple dozen levels; 256 is far above any legitimate shape.
27const MAX_DECODE_DEPTH: usize = 256;
28
29/// Floor for the per-decode element budget, so small-but-legitimate inputs
30/// still decode. A collection element that consumes zero input bytes (e.g.
31/// `Vec<()>`) cannot be bounded by remaining input length, so the budget caps
32/// the total number of collection elements materialized across one decode.
33const MIN_ELEMENT_BUDGET: u64 = 1 << 20;
34
35/// Element-budget headroom per input byte. Every byte-consuming element needs
36/// at least one input byte, so a multiple of the input length is a generous
37/// ceiling for legitimate values while still bounding zero-width blow-ups.
38const ELEMENT_BUDGET_PER_BYTE: u64 = 256;
39
40pub struct Cursor<'a> {
41    pub data: &'a [u8],
42    pub offset: usize,
43    depth: usize,
44    /// Remaining collection elements this decode may still materialize (the
45    /// operation budget from the DoS-hardening review). Bounds `Vec<()>`-style
46    /// inputs where a huge compact length drives an allocation/CPU blow-up
47    /// without consuming input bytes.
48    elements_remaining: u64,
49    /// Reject non-canonical/lossy encodings (bad bools, invalid UTF-8, invalid
50    /// Unicode scalars, non-minimal compacts) instead of silently coercing.
51    pub strict: bool,
52}
53
54impl<'a> Cursor<'a> {
55    pub fn new(data: &'a [u8]) -> Self {
56        let budget = (data.len() as u64)
57            .saturating_mul(ELEMENT_BUDGET_PER_BYTE)
58            .max(MIN_ELEMENT_BUDGET);
59        Self {
60            data,
61            offset: 0,
62            depth: 0,
63            elements_remaining: budget,
64            strict: false,
65        }
66    }
67
68    fn sequence_bytes(&mut self, len: usize) -> Result<Value, CoreError> {
69        Ok(bytes_value(self.take(len)?))
70    }
71
72    fn descend(&mut self) -> Result<(), CoreError> {
73        self.depth += 1;
74        if self.depth > MAX_DECODE_DEPTH {
75            return Err(CoreError::Codec(format!(
76                "decode recursion exceeds {MAX_DECODE_DEPTH} levels (malformed or malicious type definition)"
77            )));
78        }
79        Ok(())
80    }
81
82    fn ascend(&mut self) {
83        self.depth = self.depth.saturating_sub(1);
84    }
85
86    /// Charge one collection element against the budget before decoding it.
87    fn charge_element(&mut self) -> Result<(), CoreError> {
88        self.elements_remaining = self.elements_remaining.checked_sub(1).ok_or_else(|| {
89            CoreError::Codec(
90                "decode exceeds its element budget (malformed or malicious collection length)"
91                    .into(),
92            )
93        })?;
94        Ok(())
95    }
96
97    pub fn take(&mut self, n: usize) -> Result<&'a [u8], CoreError> {
98        let end = self.offset.checked_add(n).ok_or_else(overrun)?;
99        let slice = self.data.get(self.offset..end).ok_or_else(overrun)?;
100        self.offset = end;
101        Ok(slice)
102    }
103
104    pub fn byte(&mut self) -> Result<u8, CoreError> {
105        Ok(self.take(1)?[0])
106    }
107
108    pub fn remaining(&self) -> usize {
109        self.data.len().saturating_sub(self.offset)
110    }
111}
112
113fn overrun() -> CoreError {
114    CoreError::Codec("unexpected end of SCALE data".into())
115}
116
117/// Compact-decoded u128 (the four SCALE compact modes). In strict mode the
118/// encoding must be canonical (minimal-width for its value), matching the
119/// SCALE spec: a decoder that accepts non-minimal compacts admits multiple
120/// wire encodings for the same integer.
121pub fn compact_u128(cursor: &mut Cursor) -> Result<u128, CoreError> {
122    let strict = cursor.strict;
123    let non_canonical = || CoreError::Codec("non-canonical compact encoding".into());
124    let first = cursor.byte()?;
125    match first & 0b11 {
126        0 => Ok(u128::from(first >> 2)),
127        1 => {
128            let second = cursor.byte()?;
129            let value = u128::from(u16::from_le_bytes([first, second])) >> 2;
130            if strict && value < 0b100_0000 {
131                return Err(non_canonical());
132            }
133            Ok(value)
134        }
135        2 => {
136            let rest = cursor.take(3)?;
137            let word = u32::from_le_bytes([first, rest[0], rest[1], rest[2]]);
138            let value = u128::from(word >> 2);
139            if strict && value < 0b100_0000_0000_0000 {
140                return Err(non_canonical());
141            }
142            Ok(value)
143        }
144        _ => {
145            let len = usize::from(first >> 2) + 4;
146            if len > 16 {
147                return Err(CoreError::Codec("compact value wider than u128".into()));
148            }
149            let bytes = cursor.take(len)?;
150            // The top byte must be non-zero, otherwise a narrower mode (or
151            // fewer big-mode bytes) would encode the same value.
152            if strict && bytes.last() == Some(&0) {
153                return Err(non_canonical());
154            }
155            let mut buf = [0u8; 16];
156            buf[..len].copy_from_slice(bytes);
157            let value = u128::from_le_bytes(buf);
158            if strict && value < 0b100_0000_0000_0000_0000_0000_0000_0000 {
159                return Err(non_canonical());
160            }
161            Ok(value)
162        }
163    }
164}
165
166pub fn compact_len(cursor: &mut Cursor) -> Result<usize, CoreError> {
167    usize::try_from(compact_u128(cursor)?)
168        .map_err(|_| CoreError::Codec("length does not fit usize".into()))
169}
170
171impl Runtime {
172    /// Decode `data` as the given type spec; errors on trailing bytes when
173    /// `strict`.
174    pub fn decode_spec(
175        &self,
176        spec: &TypeSpec,
177        data: &[u8],
178        strict: bool,
179    ) -> Result<Value, CoreError> {
180        let mut cursor = Cursor::new(data);
181        cursor.strict = strict;
182        // cyscale's batch_decode fast path rendered a *top-level* zero-field
183        // struct as () while nested ones render as {}; the corpus pins both.
184        if let TypeSpec::Id(id) = spec {
185            if let Ok(ty) = self.resolve(*id) {
186                if matches!(&ty.type_def, TypeDef::Composite(c) if c.fields.is_empty()) {
187                    // A zero-field composite consumes no bytes; strict mode
188                    // must still reject trailing data on this path.
189                    if strict && !data.is_empty() {
190                        return Err(CoreError::Codec(format!(
191                            "{} undecoded bytes remain",
192                            data.len()
193                        )));
194                    }
195                    return Ok(Value::Tuple(Vec::new()));
196                }
197            }
198        }
199        let value = self.decode_value(spec, &mut cursor)?;
200        if strict && cursor.remaining() != 0 {
201            return Err(CoreError::Codec(format!(
202                "{} undecoded bytes remain",
203                cursor.remaining()
204            )));
205        }
206        Ok(value)
207    }
208
209    pub fn decode_value(&self, spec: &TypeSpec, cursor: &mut Cursor) -> Result<Value, CoreError> {
210        cursor.descend()?;
211        let value = self.decode_value_inner(spec, cursor);
212        cursor.ascend();
213        value
214    }
215
216    fn decode_value_inner(&self, spec: &TypeSpec, cursor: &mut Cursor) -> Result<Value, CoreError> {
217        match spec {
218            TypeSpec::Id(id) => self.decode_id(*id, cursor),
219            TypeSpec::Primitive(p) => self.decode_primitive_spec(*p, cursor),
220            TypeSpec::Sequence(inner) => {
221                let len = compact_len(cursor)?;
222                if matches!(**inner, TypeSpec::Primitive(Primitive::U8)) {
223                    return cursor.sequence_bytes(len);
224                }
225                let mut items = Vec::with_capacity(len.min(1024));
226                for _ in 0..len {
227                    cursor.charge_element()?;
228                    items.push(self.decode_value(inner, cursor)?);
229                }
230                Ok(Value::List(items))
231            }
232            TypeSpec::Option(inner) => match cursor.byte()? {
233                0 => Ok(Value::Null),
234                1 => self.decode_value(inner, cursor),
235                other => Err(CoreError::Codec(format!("bad Option byte {other:#x}"))),
236            },
237            TypeSpec::Array(inner, len) => {
238                let len = *len as usize;
239                // [u8; 0] renders as [] in cyscale; hex only for len > 0.
240                if len > 0 && matches!(**inner, TypeSpec::Primitive(Primitive::U8)) {
241                    return Ok(Value::hex(cursor.take(len)?));
242                }
243                let mut items = Vec::with_capacity(len.min(1024));
244                for _ in 0..len {
245                    cursor.charge_element()?;
246                    items.push(self.decode_value(inner, cursor)?);
247                }
248                Ok(Value::List(items))
249            }
250            TypeSpec::Tuple(parts) => {
251                let mut items = Vec::with_capacity(parts.len());
252                for part in parts {
253                    items.push(self.decode_value(part, cursor)?);
254                }
255                Ok(Value::Tuple(items))
256            }
257            TypeSpec::Compact(_) => Ok(uint_value(compact_u128(cursor)?)),
258            TypeSpec::Bytes => {
259                let len = compact_len(cursor)?;
260                cursor.sequence_bytes(len)
261            }
262            TypeSpec::AccountId => {
263                let raw: [u8; 32] = cursor
264                    .take(32)?
265                    .try_into()
266                    .map_err(|_| CoreError::Codec("bad AccountId length".into()))?;
267                Ok(Value::str(ss58_from_public(raw, self.ss58_format)))
268            }
269            TypeSpec::Era => self.decode_era(cursor),
270            TypeSpec::Call => self.decode_call_value(cursor),
271            TypeSpec::Extrinsic => Err(CoreError::Codec(
272                "extrinsics are decoded by the transport layer".into(),
273            )),
274        }
275    }
276
277    fn decode_primitive_spec(&self, p: Primitive, cursor: &mut Cursor) -> Result<Value, CoreError> {
278        let def = match p {
279            Primitive::Bool => TypeDefPrimitive::Bool,
280            Primitive::Char => TypeDefPrimitive::Char,
281            Primitive::Str => TypeDefPrimitive::Str,
282            Primitive::U8 => TypeDefPrimitive::U8,
283            Primitive::U16 => TypeDefPrimitive::U16,
284            Primitive::U32 => TypeDefPrimitive::U32,
285            Primitive::U64 => TypeDefPrimitive::U64,
286            Primitive::U128 => TypeDefPrimitive::U128,
287            Primitive::U256 => TypeDefPrimitive::U256,
288            Primitive::I8 => TypeDefPrimitive::I8,
289            Primitive::I16 => TypeDefPrimitive::I16,
290            Primitive::I32 => TypeDefPrimitive::I32,
291            Primitive::I64 => TypeDefPrimitive::I64,
292            Primitive::I128 => TypeDefPrimitive::I128,
293            Primitive::I256 => TypeDefPrimitive::I256,
294        };
295        decode_primitive(&def, cursor)
296    }
297
298    /// Decode one registry type. The special cases (paths with pinned
299    /// renderings) come before the structural rules.
300    pub fn decode_id(&self, id: u32, cursor: &mut Cursor) -> Result<Value, CoreError> {
301        cursor.descend()?;
302        let value = self.decode_id_inner(id, cursor);
303        cursor.ascend();
304        value
305    }
306
307    fn decode_id_inner(&self, id: u32, cursor: &mut Cursor) -> Result<Value, CoreError> {
308        if self.outer_event_type == Some(id) {
309            return Ok(self.decode_outer_event(cursor)?.into_value());
310        }
311        if self.extrinsic.call_type == Some(id) {
312            return self.decode_call_value(cursor);
313        }
314        let ty = self.resolve(id)?;
315        let segments = &ty.path.segments;
316        let last = segments.last().map(String::as_str);
317        if last == Some("EventRecord")
318            && segments.first().map(String::as_str) == Some("frame_system")
319        {
320            return self.decode_event_record(ty, cursor);
321        }
322
323        match last {
324            Some("AccountId32") if is_path(segments, &["sp_core", "crypto", "AccountId32"]) => {
325                let raw: [u8; 32] = cursor
326                    .take(32)?
327                    .try_into()
328                    .map_err(|_| CoreError::Codec("bad AccountId32".into()))?;
329                return Ok(Value::str(ss58_from_public(raw, self.ss58_format)));
330            }
331            Some("Era") if segments.first().map(String::as_str) == Some("sp_runtime") => {
332                return self.decode_era(cursor);
333            }
334            Some("MultiAddress") => {
335                return self.decode_multiaddress(ty, cursor);
336            }
337            // cyscale registered primitive_types::U256 as a 32-byte LE int.
338            Some("U256") if is_path(segments, &["primitive_types", "U256"]) => {
339                return Ok(Value::U256(fixed(cursor.take(32)?)?));
340            }
341            Some("BTreeMap") => {
342                return self.decode_btreemap(ty, cursor);
343            }
344            _ => {}
345        }
346
347        match &ty.type_def {
348            TypeDef::Primitive(p) => decode_primitive(p, cursor),
349            TypeDef::Compact(c) => {
350                // Compact of a newtype (e.g. Compact<IndexU32>) unwraps like
351                // the plain compact int cyscale produced. The metadata's
352                // declared width is not a reliable bound on this chain (real
353                // recorded compacts exceed their nominal `type_param`), so it
354                // is intentionally not enforced.
355                let _ = c;
356                Ok(uint_value(compact_u128(cursor)?))
357            }
358            TypeDef::Composite(composite) => self.decode_fields_owner(&composite.fields, cursor),
359            TypeDef::Variant(variant) => {
360                let is_option = last == Some("Option") && segments.len() == 1;
361                let index = cursor.byte()?;
362                let chosen = variant
363                    .variants
364                    .iter()
365                    .find(|v| v.index == index)
366                    .ok_or_else(|| {
367                        CoreError::Codec(format!("no variant with index {index} in type {id}"))
368                    })?;
369                if is_option {
370                    return match chosen.name.as_str() {
371                        "None" => Ok(Value::Null),
372                        _ => self.decode_fields_payload(&chosen.fields, cursor),
373                    };
374                }
375                self.decode_variant(chosen, cursor)
376            }
377            TypeDef::Sequence(s) => {
378                let len = compact_len(cursor)?;
379                if self.is_u8(s.type_param.id) {
380                    return cursor.sequence_bytes(len);
381                }
382                let mut items = Vec::with_capacity(len.min(4096));
383                for _ in 0..len {
384                    cursor.charge_element()?;
385                    items.push(self.decode_id(s.type_param.id, cursor)?);
386                }
387                Ok(Value::List(items))
388            }
389            TypeDef::Array(a) => {
390                let len = a.len as usize;
391                if len > 0 && self.is_u8(a.type_param.id) {
392                    return Ok(Value::hex(cursor.take(len)?));
393                }
394                let mut items = Vec::with_capacity(len.min(4096));
395                for _ in 0..len {
396                    cursor.charge_element()?;
397                    items.push(self.decode_id(a.type_param.id, cursor)?);
398                }
399                Ok(Value::List(items))
400            }
401            TypeDef::Tuple(t) => {
402                let mut items = Vec::with_capacity(t.fields.len());
403                for field in &t.fields {
404                    items.push(self.decode_id(field.id, cursor)?);
405                }
406                Ok(Value::Tuple(items))
407            }
408            TypeDef::BitSequence(_) => Err(CoreError::Codec(
409                "bit sequences are not used by this chain".into(),
410            )),
411        }
412    }
413
414    fn is_u8(&self, id: u32) -> bool {
415        matches!(
416            self.types.resolve(id).map(|t| &t.type_def),
417            Some(TypeDef::Primitive(TypeDefPrimitive::U8))
418        )
419    }
420
421    /// Composite-body shapes: named fields -> dict, one unnamed -> unwrap,
422    /// several unnamed -> tuple, none -> empty dict (cyscale's zero-sized
423    /// struct rendering).
424    fn decode_fields_owner(
425        &self,
426        fields: &[Field<PortableForm>],
427        cursor: &mut Cursor,
428    ) -> Result<Value, CoreError> {
429        if fields.is_empty() {
430            return Ok(Value::Dict(Vec::new()));
431        }
432        let named = fields.iter().all(|f| f.name.is_some());
433        if named {
434            let mut entries = Vec::with_capacity(fields.len());
435            for field in fields {
436                let name = field.name.clone().unwrap_or_default();
437                entries.push((Value::Str(name), self.decode_id(field.ty.id, cursor)?));
438            }
439            return Ok(Value::Dict(entries));
440        }
441        if fields.len() == 1 {
442            return self.decode_id(fields[0].ty.id, cursor);
443        }
444        let mut items = Vec::with_capacity(fields.len());
445        for field in fields {
446            items.push(self.decode_id(field.ty.id, cursor)?);
447        }
448        Ok(Value::Tuple(items))
449    }
450
451    /// A chosen enum variant's payload (same field rules as composites).
452    fn decode_fields_payload(
453        &self,
454        fields: &[Field<PortableForm>],
455        cursor: &mut Cursor,
456    ) -> Result<Value, CoreError> {
457        self.decode_fields_owner(fields, cursor)
458    }
459
460    fn decode_variant(
461        &self,
462        chosen: &Variant<PortableForm>,
463        cursor: &mut Cursor,
464    ) -> Result<Value, CoreError> {
465        if chosen.fields.is_empty() {
466            return Ok(Value::Str(chosen.name.clone()));
467        }
468        let payload = self.decode_fields_payload(&chosen.fields, cursor)?;
469        Ok(Value::Dict(vec![(
470            Value::Str(chosen.name.clone()),
471            payload,
472        )]))
473    }
474
475    /// MultiAddress: `Id` renders as the bare ss58 string and `Index` as the
476    /// bare integer (cyscale's GenericMultiAddress); other variants keep the
477    /// standard enum shape.
478    fn decode_multiaddress(
479        &self,
480        ty: &scale_info::Type<PortableForm>,
481        cursor: &mut Cursor,
482    ) -> Result<Value, CoreError> {
483        let TypeDef::Variant(variant) = &ty.type_def else {
484            return Err(CoreError::Codec("MultiAddress is not an enum".into()));
485        };
486        let index = cursor.byte()?;
487        let chosen = variant
488            .variants
489            .iter()
490            .find(|v| v.index == index)
491            .ok_or_else(|| CoreError::Codec(format!("bad MultiAddress variant {index}")))?;
492        match chosen.name.as_str() {
493            "Id" | "Index" => self.decode_fields_payload(&chosen.fields, cursor),
494            _ => self.decode_variant(chosen, cursor),
495        }
496    }
497
498    /// BTreeMap<K, V>: a length-prefixed list of pairs rendered as a dict
499    /// with native keys.
500    fn decode_btreemap(
501        &self,
502        ty: &scale_info::Type<PortableForm>,
503        cursor: &mut Cursor,
504    ) -> Result<Value, CoreError> {
505        // Portable form: composite with a single Vec<(K, V)> field.
506        let TypeDef::Composite(composite) = &ty.type_def else {
507            return Err(CoreError::Codec("BTreeMap is not a composite".into()));
508        };
509        let inner = composite
510            .fields
511            .first()
512            .map(|f| f.ty.id)
513            .ok_or_else(|| CoreError::Codec("BTreeMap has no inner field".into()))?;
514        let TypeDef::Sequence(seq) = &self.resolve(inner)?.type_def else {
515            return Err(CoreError::Codec("BTreeMap inner is not a sequence".into()));
516        };
517        let TypeDef::Tuple(pair) = &self.resolve(seq.type_param.id)?.type_def else {
518            return Err(CoreError::Codec("BTreeMap pair is not a tuple".into()));
519        };
520        let (key_ty, value_ty) = match pair.fields.as_slice() {
521            [k, v] => (k.id, v.id),
522            _ => return Err(CoreError::Codec("BTreeMap pair is not 2-ary".into())),
523        };
524        let len = compact_len(cursor)?;
525        let mut entries = Vec::with_capacity(len.min(4096));
526        for _ in 0..len {
527            cursor.charge_element()?;
528            let key = self.decode_id(key_ty, cursor)?;
529            let value = self.decode_id(value_ty, cursor)?;
530            entries.push((key, value));
531        }
532        Ok(Value::Dict(entries))
533    }
534
535    /// The outer RuntimeEvent in cyscale's GenericEvent shape:
536    /// `{"event_index": "XXYY", "module_id", "event_id", "attributes"}`
537    /// (unit payloads render as null).
538    fn decode_outer_event(&self, cursor: &mut Cursor) -> Result<DecodedEvent, CoreError> {
539        let outer_id = self
540            .outer_event_type
541            .ok_or_else(|| CoreError::Codec("runtime has no outer event enum".into()))?;
542        let TypeDef::Variant(outer) = &self.resolve(outer_id)?.type_def else {
543            return Err(CoreError::Codec("outer event type is not an enum".into()));
544        };
545        let pallet_index = cursor.byte()?;
546        let pallet_variant = outer
547            .variants
548            .iter()
549            .find(|v| v.index == pallet_index)
550            .ok_or_else(|| CoreError::Codec(format!("no event pallet at index {pallet_index}")))?;
551        let inner_id = pallet_variant
552            .fields
553            .first()
554            .map(|f| f.ty.id)
555            .ok_or_else(|| CoreError::Codec("outer event variant has no payload".into()))?;
556        let TypeDef::Variant(inner) = &self.resolve(inner_id)?.type_def else {
557            return Err(CoreError::Codec("pallet event type is not an enum".into()));
558        };
559        let event_index = cursor.byte()?;
560        let event = inner
561            .variants
562            .iter()
563            .find(|v| v.index == event_index)
564            .ok_or_else(|| {
565                CoreError::Codec(format!(
566                    "no event at index {event_index} in pallet {}",
567                    pallet_variant.name
568                ))
569            })?;
570        let attributes = if event.fields.is_empty() {
571            Value::Null
572        } else {
573            self.decode_fields_payload(&event.fields, cursor)?
574        };
575        Ok(DecodedEvent {
576            pallet_index,
577            event_index,
578            module_id: pallet_variant.name.clone(),
579            event_id: event.name.clone(),
580            attributes,
581        })
582    }
583
584    /// `frame_system::EventRecord` in cyscale's flattened shape: the phase
585    /// split into name + extrinsic_idx, the event dict, and the event's
586    /// fields copied to the top level.
587    fn decode_event_record(
588        &self,
589        ty: &scale_info::Type<PortableForm>,
590        cursor: &mut Cursor,
591    ) -> Result<Value, CoreError> {
592        let TypeDef::Composite(composite) = &ty.type_def else {
593            return Err(CoreError::Codec("EventRecord is not a composite".into()));
594        };
595        let field_id = |name: &str| -> Result<u32, CoreError> {
596            composite
597                .fields
598                .iter()
599                .find(|f| f.name.as_deref() == Some(name))
600                .map(|f| f.ty.id)
601                .ok_or_else(|| CoreError::Codec(format!("EventRecord has no {name} field")))
602        };
603        // phase: ApplyExtrinsic(u32) carries the index; the other variants
604        // render as their bare names.
605        let phase_id = field_id("phase")?;
606        let TypeDef::Variant(phase_def) = &self.resolve(phase_id)?.type_def else {
607            return Err(CoreError::Codec("Phase is not an enum".into()));
608        };
609        let phase_index = cursor.byte()?;
610        let phase = phase_def
611            .variants
612            .iter()
613            .find(|v| v.index == phase_index)
614            .ok_or_else(|| CoreError::Codec(format!("bad Phase variant {phase_index}")))?;
615        let extrinsic_idx = if phase.fields.is_empty() {
616            Value::Null
617        } else {
618            self.decode_fields_payload(&phase.fields, cursor)?
619        };
620
621        let event = self.decode_outer_event(cursor)?;
622
623        let topics_id = field_id("topics")?;
624        let topics = self.decode_id(topics_id, cursor)?;
625
626        Ok(Value::record(vec![
627            ("phase".into(), Value::Str(phase.name.clone())),
628            ("extrinsic_idx".into(), extrinsic_idx),
629            ("event".into(), event.clone().into_value()),
630            (
631                "event_index".into(),
632                Value::Int(i128::from(event.pallet_index)),
633            ),
634            ("module_id".into(), Value::Str(event.module_id.clone())),
635            ("event_id".into(), Value::Str(event.event_id.clone())),
636            ("attributes".into(), event.attributes.clone()),
637            ("topics".into(), topics),
638        ]))
639    }
640
641    /// Era: `"00"` for immortal, `(period, phase)` for mortal — cyscale's
642    /// GenericEra values.
643    fn decode_era(&self, cursor: &mut Cursor) -> Result<Value, CoreError> {
644        let first = cursor.byte()?;
645        if first == 0 {
646            return Ok(Value::str("00"));
647        }
648        let second = cursor.byte()?;
649        let encoded = u64::from(u16::from_le_bytes([first, second]));
650        let period = 2u64 << (encoded % 16);
651        let quantize_factor = (period >> 12).max(1);
652        let phase = (encoded >> 4) * quantize_factor;
653        Ok(Value::Tuple(vec![
654            Value::Int(i128::from(period)),
655            Value::Int(i128::from(phase)),
656        ]))
657    }
658
659    /// The runtime's outer Call in cyscale's metadata-aware shape:
660    /// `{call_index, call_function, call_module, call_args: [{name, type,
661    /// value}], call_hash}`.
662    pub fn decode_call_value(&self, cursor: &mut Cursor) -> Result<Value, CoreError> {
663        let start = cursor.offset;
664        let pallet_index = cursor.byte()?;
665        let call_index = cursor.byte()?;
666        let pallet = self
667            .pallet_at(pallet_index)
668            .ok_or_else(|| CoreError::Codec(format!("no pallet at index {pallet_index}")))?;
669        let calls_type = pallet
670            .calls_type
671            .ok_or_else(|| CoreError::Codec(format!("pallet {} has no calls", pallet.name)))?;
672        let TypeDef::Variant(variant) = &self.resolve(calls_type)?.type_def else {
673            return Err(CoreError::Codec("call type is not an enum".into()));
674        };
675        let function = variant
676            .variants
677            .iter()
678            .find(|v| v.index == call_index)
679            .ok_or_else(|| {
680                CoreError::Codec(format!(
681                    "no call at index {call_index} in pallet {}",
682                    pallet.name
683                ))
684            })?;
685        let mut args = Vec::with_capacity(function.fields.len());
686        for field in &function.fields {
687            let value = self.decode_id(field.ty.id, cursor)?;
688            args.push(Value::record(vec![
689                (
690                    "name".into(),
691                    Value::Str(field.name.clone().unwrap_or_default()),
692                ),
693                (
694                    "type".into(),
695                    Value::Str(convert_type_string(
696                        field.type_name.as_deref().unwrap_or_default(),
697                    )),
698                ),
699                ("value".into(), value),
700            ]));
701        }
702        let call_bytes = &cursor.data[start..cursor.offset];
703        let call_hash = sp_core::hashing::blake2_256(call_bytes);
704        Ok(Value::record(vec![
705            (
706                "call_index".into(),
707                Value::str(format!("0x{:02x}{:02x}", pallet_index, call_index)),
708            ),
709            ("call_function".into(), Value::Str(function.name.clone())),
710            ("call_module".into(), Value::Str(pallet.name.clone())),
711            ("call_args".into(), Value::List(args)),
712            ("call_hash".into(), Value::hex(&call_hash)),
713        ]))
714    }
715}
716
717/// One decoded outer event, before shaping.
718#[derive(Clone)]
719struct DecodedEvent {
720    pallet_index: u8,
721    event_index: u8,
722    module_id: String,
723    event_id: String,
724    attributes: Value,
725}
726
727impl DecodedEvent {
728    fn into_value(self) -> Value {
729        Value::record(vec![
730            (
731                "event_index".into(),
732                Value::str(format!("{:02x}{:02x}", self.pallet_index, self.event_index)),
733            ),
734            ("module_id".into(), Value::Str(self.module_id)),
735            ("event_id".into(), Value::Str(self.event_id)),
736            ("attributes".into(), self.attributes),
737        ])
738    }
739}
740
741/// cyscale's `convert_type_string`: normalize a metadata `typeName` the way
742/// the old codec rendered call-arg types (`T::Balance` -> `Balance`,
743/// `Box<<T as Config>::RuntimeCall>` -> `RuntimeCall`, `Vec<u8>` -> `Bytes`).
744pub fn convert_type_string(name: &str) -> String {
745    fn remove_ci(haystack: &mut String, needle: &str) {
746        let lower_needle = needle.to_lowercase();
747        loop {
748            let lower = haystack.to_lowercase();
749            let Some(pos) = lower.find(&lower_needle) else {
750                break;
751            };
752            haystack.replace_range(pos..pos.saturating_add(needle.len()), "");
753        }
754    }
755
756    let mut name = name.replace("T::", "");
757    if name.to_lowercase().starts_with("t::") {
758        name.replace_range(..3, "");
759    }
760    remove_ci(&mut name, "<T>");
761    remove_ci(&mut name, "<T as Trait>::");
762    remove_ci(&mut name, "<T as Trait<I>>::");
763    remove_ci(&mut name, "<T as Config>::");
764    remove_ci(&mut name, "<T as Config<I>>::");
765    name = name.replace('\n', "");
766    for prefix in [
767        "grandpa::",
768        "session::",
769        "slashing::",
770        "limits::",
771        "beefy_primitives::",
772        "xcm::opaque::",
773    ] {
774        if let Some(stripped) = name.strip_prefix(prefix) {
775            name = stripped.to_string();
776            break;
777        }
778    }
779    // VecDeque<...> renders as Vec<...> (case-insensitive replace).
780    loop {
781        let lower = name.to_lowercase();
782        let Some(pos) = lower.find("vecdeque<") else {
783            break;
784        };
785        name.replace_range(pos..pos.saturating_add("VecDeque<".len()), "Vec<");
786    }
787    let lower = name.to_lowercase();
788    if lower.starts_with("box<") && lower.ends_with('>') && name.len() > 5 {
789        name = name[4..name.len().saturating_sub(1)].to_string();
790    }
791
792    match name.to_lowercase().as_str() {
793        _ if name == "()" => "Null".into(),
794        "vec<u8>" | "&[u8]" | "& 'static[u8]" => "Bytes".into(),
795        "<lookup as staticlookup>::source" => "LookupSource".into(),
796        "<balance as hascompact>::type" => "Compact<Balance>".into(),
797        "<blocknumber as hascompact>::type" => "Compact<BlockNumber>".into(),
798        "<moment as hascompact>::type" => "Compact<Moment>".into(),
799        "<inherentofflinereport as inherentofflinereport>::inherent" => {
800            "InherentOfflineReport".into()
801        }
802        _ => name,
803    }
804}
805
806fn is_path(segments: &[String], expected: &[&str]) -> bool {
807    segments.len() == expected.len() && segments.iter().zip(expected).all(|(a, b)| a == b)
808}
809
810fn uint_value(v: u128) -> Value {
811    match i128::try_from(v) {
812        Ok(i) => Value::Int(i),
813        Err(_) => Value::Uint(v),
814    }
815}
816
817fn bytes_value(data: &[u8]) -> Value {
818    match core::str::from_utf8(data) {
819        Ok(s) => Value::str(s),
820        Err(_) => Value::hex(data),
821    }
822}
823
824fn decode_primitive(p: &TypeDefPrimitive, cursor: &mut Cursor) -> Result<Value, CoreError> {
825    use TypeDefPrimitive as P;
826    let strict = cursor.strict;
827    Ok(match p {
828        P::Bool => {
829            let byte = cursor.byte()?;
830            if strict && byte > 1 {
831                return Err(CoreError::Codec(format!(
832                    "non-canonical bool byte {byte:#x}"
833                )));
834            }
835            Value::Bool(byte != 0)
836        }
837        P::Char => {
838            let raw = cursor.take(4)?;
839            let code = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
840            match char::from_u32(code) {
841                Some(c) => Value::Str(c.to_string()),
842                None if strict => {
843                    return Err(CoreError::Codec(format!(
844                        "invalid Unicode scalar value {code:#x}"
845                    )))
846                }
847                None => Value::Str('\u{fffd}'.to_string()),
848            }
849        }
850        P::Str => {
851            let len = compact_len(cursor)?;
852            let raw = cursor.take(len)?;
853            if strict {
854                let s = core::str::from_utf8(raw)
855                    .map_err(|e| CoreError::Codec(format!("invalid UTF-8 string: {e}")))?;
856                Value::Str(s.to_owned())
857            } else {
858                Value::Str(String::from_utf8_lossy(raw).into_owned())
859            }
860        }
861        P::U8 => Value::Int(i128::from(cursor.byte()?)),
862        P::U16 => Value::Int(i128::from(u16::from_le_bytes(fixed(cursor.take(2)?)?))),
863        P::U32 => Value::Int(i128::from(u32::from_le_bytes(fixed(cursor.take(4)?)?))),
864        P::U64 => Value::Int(i128::from(u64::from_le_bytes(fixed(cursor.take(8)?)?))),
865        P::U128 => uint_value(u128::from_le_bytes(fixed(cursor.take(16)?)?)),
866        P::U256 => Value::U256(fixed(cursor.take(32)?)?),
867        P::I8 => Value::Int(i128::from(cursor.byte()? as i8)),
868        P::I16 => Value::Int(i128::from(i16::from_le_bytes(fixed(cursor.take(2)?)?))),
869        P::I32 => Value::Int(i128::from(i32::from_le_bytes(fixed(cursor.take(4)?)?))),
870        P::I64 => Value::Int(i128::from(i64::from_le_bytes(fixed(cursor.take(8)?)?))),
871        P::I128 => Value::Int(i128::from_le_bytes(fixed(cursor.take(16)?)?)),
872        P::I256 => {
873            // Rendered like U256 with sign interpretation left to the corpus
874            // (the chain does not use i256; kept for registry completeness).
875            Value::U256(fixed(cursor.take(32)?)?)
876        }
877    })
878}
879
880fn fixed<const N: usize>(slice: &[u8]) -> Result<[u8; N], CoreError> {
881    slice
882        .try_into()
883        .map_err(|_| CoreError::Codec("bad fixed-width slice".into()))
884}