Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Tuple expression support in register values
From R8 register at https://explorer.ergoplatform.com/en/transactions/b5fd96c9f8c1ff436d609a225a12377c6873b890a145fc05f4099845b89315e5

```
R8:
serializedValue: 860202660263
sigmaType: (SByte, SByte)
renderedValue: [102,99]
```
"860202660263" reg value fails to parse with `InvalidTypeCode(134)` error.

The reason for the error is that instead an ErgoTree data value an ErgoTree expression is encoded. `0x86` is the op code for [`Tuple` expression](https://github.com/ergoplatform/sigma-rust/blob/e7bebc6ef7c7c6f5c05e4ae72ccd2e5690100284/ergotree-ir/src/mir/tuple.rs#L17-L23) following by `0x02` (size), `0x0266` (SByte type `0x02`, value `0x66`), `0x0263` (SByte type `0x02`, value `0x63`). 

The ErgoTree expression is not a data value, and the proper encoding for `(102, 99)` byte tuple would be `566663` (`0x56` for symmetrical pair (tuple) of bytes) followed by the bytes themselves `0x6663`. However, tuple expression with constant value is allowed as a register value (see https://github.com/ScorexFoundation/sigmastate-interpreter/blob/08627b714f04794b8f8cc785452aa8dc58bc9c48/sigmastate/src/main/scala/sigmastate/Values.scala#L857-L866)



Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/ergo_box.rs b/bindings/ergo-lib-c-core/src/ergo_box.rs
index d46f83c4f..03dafa43d 100644
--- a/bindings/ergo-lib-c-core/src/ergo_box.rs
+++ b/bindings/ergo-lib-c-core/src/ergo_box.rs
@@ -138,9 +138,9 @@ pub unsafe fn ergo_box_candidate_register_value(
     if let Some(c) = candidate
         .0
         .additional_registers
-        .get_constant(register_id.into())
+        .get_constant(register_id.into())?
     {
-        *constant_out = Box::into_raw(Box::new(Constant(c.clone())));
+        *constant_out = Box::into_raw(Box::new(Constant(c)));
         Ok(true)
     } else {
         Ok(false)
@@ -293,9 +293,9 @@ pub unsafe fn ergo_box_register_value(
     if let Some(c) = ergo_box
         .0
         .additional_registers
-        .get_constant(register_id.into())
+        .get_constant(register_id.into())?
     {
-        *constant_out = Box::into_raw(Box::new(Constant(c.clone())));
+        *constant_out = Box::into_raw(Box::new(Constant(c)));
         Ok(true)
     } else {
         Ok(false)
diff --git a/bindings/ergo-lib-c-core/src/error_conversion.rs b/bindings/ergo-lib-c-core/src/error_conversion.rs
index ed9c64ba4..c1a72b01c 100644
--- a/bindings/ergo-lib-c-core/src/error_conversion.rs
+++ b/bindings/ergo-lib-c-core/src/error_conversion.rs
@@ -13,6 +13,7 @@ use ergo_lib::ergotree_interpreter::sigma_protocol::verifier::VerifierError;
 use ergo_lib::ergotree_ir::chain::address::AddressEncoderError;
 use ergo_lib::ergotree_ir::chain::address::AddressError;
 use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValueError;
+use ergo_lib::ergotree_ir::chain::ergo_box::RegisterValueError;
 use ergo_lib::ergotree_ir::chain::token::TokenAmountError;
 use ergo_lib::wallet::derivation_path::ChildIndexError;
 use ergo_lib::wallet::signing::TxSigningError;
@@ -75,3 +76,4 @@ convert_error_via_debug!(BoundedVecOutOfBounds);
 convert_error_via_debug!(TransactionError);
 convert_error_via_debug!(NipopowProofError);
 convert_error_via_debug!(VerifierError);
+convert_error_via_debug!(RegisterValueError);
diff --git a/bindings/ergo-lib-wasm/src/ergo_box.rs b/bindings/ergo-lib-wasm/src/ergo_box.rs
index b0b45d950..ba30e053e 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box.rs
@@ -84,12 +84,16 @@ impl ErgoBoxCandidate {
     }
 
     /// Returns value (ErgoTree constant) stored in the register or None if the register is empty or cannot be parsed
-    pub fn register_value(&self, register_id: NonMandatoryRegisterId) -> Option<Constant> {
-        self.0
+    pub fn register_value(
+        &self,
+        register_id: NonMandatoryRegisterId,
+    ) -> Result<Option<Constant>, JsValue> {
+        Ok(self
+            .0
             .additional_registers
             .get_constant(register_id.into())
-            .cloned()
-            .map(Constant::from)
+            .map_err(to_js)?
+            .map(Constant::from))
     }
 
     /// Get box creation height
@@ -196,12 +200,16 @@ impl ErgoBox {
     }
 
     /// Returns value (ErgoTree constant) stored in the register or None if the register is empty or cannot be parsed
-    pub fn register_value(&self, register_id: NonMandatoryRegisterId) -> Option<Constant> {
-        self.0
+    pub fn register_value(
+        &self,
+        register_id: NonMandatoryRegisterId,
+    ) -> Result<Option<Constant>, JsValue> {
+        Ok(self
+            .0
             .additional_registers
             .get_constant(register_id.into())
-            .cloned()
-            .map(Constant::from)
+            .map_err(to_js)?
+            .map(Constant::from))
     }
 
     /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
diff --git a/bindings/ergo-lib-wasm/src/error_conversion.rs b/bindings/ergo-lib-wasm/src/error_conversion.rs
index 275f76183..b38ae2e93 100644
--- a/bindings/ergo-lib-wasm/src/error_conversion.rs
+++ b/bindings/ergo-lib-wasm/src/error_conversion.rs
@@ -16,6 +16,7 @@ use ergo_lib::ergotree_interpreter::sigma_protocol::verifier::VerifierError;
 use ergo_lib::ergotree_ir::chain::address::AddressEncoderError;
 use ergo_lib::ergotree_ir::chain::address::AddressError;
 use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValueError;
+use ergo_lib::ergotree_ir::chain::ergo_box::RegisterValueError;
 use ergo_lib::ergotree_ir::chain::token::TokenAmountError;
 use ergo_lib::wallet::derivation_path::ChildIndexError;
 use ergo_lib::wallet::derivation_path::DerivationPathError;
@@ -96,6 +97,7 @@ from_error_to_wrap!(ParseError);
 from_error_to_wrap!(ConvError);
 from_error_to_wrap!(TransactionContextError);
 from_error_to_wrap!(TxVerifyError);
+from_error_to_wrap!(RegisterValueError);
 
 macro_rules! from_error_to_wrap_via_debug {
     ($t:ident) => {
diff --git a/ergo-lib/src/chain/ergo_box/box_builder.rs b/ergo-lib/src/chain/ergo_box/box_builder.rs
index 5facb8170..ed75539e2 100644
--- a/ergo-lib/src/chain/ergo_box/box_builder.rs
+++ b/ergo-lib/src/chain/ergo_box/box_builder.rs
@@ -348,7 +348,10 @@ mod tests {
         builder.set_register_value(R4, reg_value.clone());
         assert_eq!(builder.register_value(&R4).unwrap(), &reg_value);
         let b = builder.build().unwrap();
-        assert_eq!(b.additional_registers.get_constant(R4).unwrap(), &reg_value);
+        assert_eq!(
+            b.additional_registers.get_constant(R4).unwrap().unwrap(),
+            reg_value
+        );
     }
 
     #[test]
@@ -360,7 +363,7 @@ mod tests {
         builder.delete_register_value(&R4);
         assert!(builder.register_value(&R4).is_none());
         let b = builder.build().unwrap();
-        assert!(b.additional_registers.get_constant(R4).is_none());
+        assert!(b.additional_registers.get_constant(R4).unwrap().is_none());
     }
 
     #[test]
@@ -384,6 +387,7 @@ mod tests {
                 .additional_registers
                 .get_constant(NonMandatoryRegisterId::R4)
                 .unwrap()
+                .unwrap()
                 .base16_str()
                 .unwrap(),
             "0e03555344",
@@ -394,6 +398,7 @@ mod tests {
                 .additional_registers
                 .get_constant(NonMandatoryRegisterId::R5)
                 .unwrap()
+                .unwrap()
                 .base16_str()
                 .unwrap(),
             "0e184e6f7468696e67206261636b65642055534420746f6b656e",
@@ -404,6 +409,7 @@ mod tests {
                 .additional_registers
                 .get_constant(NonMandatoryRegisterId::R6)
                 .unwrap()
+                .unwrap()
                 .base16_str()
                 .unwrap(),
             "0e0132",
diff --git a/ergotree-interpreter/src/eval/deserialize_register.rs b/ergotree-interpreter/src/eval/deserialize_register.rs
index f85f119a8..92d4dfad0 100644
--- a/ergotree-interpreter/src/eval/deserialize_register.rs
+++ b/ergotree-interpreter/src/eval/deserialize_register.rs
@@ -23,7 +23,7 @@ impl Evaluable for DeserializeRegister {
                     e
                 ))
             })?) {
-            Some(c) => {
+            Ok(Some(c)) => {
                 if c.tpe != SType::SColl(SType::SByte.into()) {
                     Err(EvalError::UnexpectedExpr(format!(
                         "DeserializeRegister: expected value to have type SColl(SByte), got {:?}",
@@ -39,16 +39,17 @@ impl Evaluable for DeserializeRegister {
                     }
                 }
             }
-            None => match &self.default {
-                Some(default_expr) => {
-                    if default_expr.tpe() != self.tpe {
-                        Err(EvalError::UnexpectedExpr(format!("DeserializeRegister: expected default expr to have type {:?}, got {:?}", self.tpe, default_expr.tpe())))
-                    } else {
-                        default_expr.eval(env, ctx)
-                    }
-                }
+            Ok(None) => match &self.default {
+                Some(default_expr) => eval_default(&self.tpe, default_expr, env, ctx),
+                None => Err(EvalError::NotFound(format!(
+                    "DeserializeRegister: register {:?} is empty",
+                    self.reg
+                ))),
+            },
+            Err(e) => match &self.default {
+                Some(default_expr) => eval_default(&self.tpe, default_expr, env, ctx),
                 None => Err(EvalError::NotFound(format!(
-                    "DeserializeRegister: register with id {} is empty",
+                    "DeserializeRegister: failed to get the register id {} with error: {e:?}",
                     self.reg
                 ))),
             },
@@ -56,6 +57,23 @@ impl Evaluable for DeserializeRegister {
     }
 }
 
+fn eval_default(
+    deserialize_reg_tpe: &SType,
+    default_expr: &Expr,
+    env: &Env,
+    ctx: &mut EvalContext,
+) -> Result<Value, EvalError> {
+    if &default_expr.tpe() != deserialize_reg_tpe {
+        Err(EvalError::UnexpectedExpr(format!(
+            "DeserializeRegister: expected default expr to have type {:?}, got {:?}",
+            deserialize_reg_tpe,
+            default_expr.tpe()
+        )))
+    } else {
+        default_expr.eval(env, ctx)
+    }
+}
+
 #[allow(clippy::unwrap_used)]
 #[cfg(feature = "arbitrary")]
 #[cfg(test)]
diff --git a/ergotree-interpreter/src/eval/extract_reg_as.rs b/ergotree-interpreter/src/eval/extract_reg_as.rs
index 2bed11268..a9bd6efa7 100644
--- a/ergotree-interpreter/src/eval/extract_reg_as.rs
+++ b/ergotree-interpreter/src/eval/extract_reg_as.rs
@@ -17,16 +17,16 @@ impl Evaluable for ExtractRegisterAs {
             .input
             .eval(env, ctx)?
             .try_extract_into::<Arc<ErgoBox>>()?;
-        Ok(Value::Opt(Box::new(
-            ir_box
-                .get_register(self.register_id.try_into().map_err(|e| {
-                    EvalError::RegisterIdOutOfBounds(format!(
-                        "register index is out of bounds: {:?} ",
-                        e
-                    ))
-                })?)
-                .map(|c| Value::from(c.v)),
-        )))
+        let id = self.register_id.try_into().map_err(|e| {
+            EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
+        })?;
+        let reg_val_opt = ir_box.get_register(id).map_err(|e| {
+            EvalError::NotFound(format!(
+                "Error getting the register id {:?} with error {e:?}",
+                id
+            ))
+        })?;
+        Ok(Value::Opt(Box::new(reg_val_opt.map(|c| Value::from(c.v)))))
     }
 }
 
diff --git a/ergotree-interpreter/src/eval/sbox.rs b/ergotree-interpreter/src/eval/sbox.rs
index fa19a8907..aa28cf416 100644
--- a/ergotree-interpreter/src/eval/sbox.rs
+++ b/ergotree-interpreter/src/eval/sbox.rs
@@ -16,21 +16,25 @@ pub(crate) static VALUE_EVAL_FN: EvalFn = |_env, _ctx, obj, _args| {
 };
 
 pub(crate) static GET_REG_EVAL_FN: EvalFn = |_env, _ctx, obj, args| {
+    let reg_id = args
+        .get(0)
+        .cloned()
+        .ok_or_else(|| EvalError::NotFound("register index is missing".to_string()))?
+        .try_extract_into::<i8>()?
+        .try_into()
+        .map_err(|e| {
+            EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
+        })?;
+
     Ok(Value::Opt(Box::new(
         obj.try_extract_into::<Arc<ErgoBox>>()?
-            .get_register(
-                args.get(0)
-                    .cloned()
-                    .ok_or_else(|| EvalError::NotFound("register index is missing".to_string()))?
-                    .try_extract_into::<i8>()?
-                    .try_into()
-                    .map_err(|e| {
-                        EvalError::RegisterIdOutOfBounds(format!(
-                            "register index is out of bounds: {:?} ",
-                            e
-                        ))
-                    })?,
-            )
+            .get_register(reg_id)
+            .map_err(|e| {
+                EvalError::NotFound(format!(
+                    "Error getting the register id {:?} with error {e:?}",
+                    reg_id
+                ))
+            })?
             .map(|c| Value::from(c.v)),
     )))
 };
diff --git a/ergotree-ir/src/chain/ergo_box.rs b/ergotree-ir/src/chain/ergo_box.rs
index 7c345831d..b372dc44c 100644
--- a/ergotree-ir/src/chain/ergo_box.rs
+++ b/ergotree-ir/src/chain/ergo_box.rs
@@ -147,8 +147,8 @@ impl ErgoBox {
     }
 
     /// Get register value, or None if register is empty or cannot be parsed
-    pub fn get_register(&self, id: RegisterId) -> Option<Constant> {
-        match id {
+    pub fn get_register(&self, id: RegisterId) -> Result<Option<Constant>, RegisterValueError> {
+        Ok(match id {
             RegisterId::MandatoryRegisterId(id) => match id {
                 MandatoryRegisterId::R0 => Some(self.value.into()),
                 // chance of box script is not serializable are tiny comparing to returning Result
@@ -157,10 +157,8 @@ impl ErgoBox {
                 MandatoryRegisterId::R2 => Some(self.tokens_raw().into()),
                 MandatoryRegisterId::R3 => Some(self.creation_info().into()),
             },
-            RegisterId::NonMandatoryRegisterId(id) => {
-                self.additional_registers.get_constant(id).cloned()
-            }
-        }
+            RegisterId::NonMandatoryRegisterId(id) => self.additional_registers.get_constant(id)?,
+        })
     }
 
     /// Returns tokens as tuple of byte array and amount as primitive types
@@ -460,17 +458,20 @@ mod tests {
     #[test]
     fn get_register_mandatory() {
         let b = force_any_val::<ErgoBox>();
-        assert_eq!(b.get_register(RegisterId::R0).unwrap(), b.value.into());
         assert_eq!(
-            b.get_register(RegisterId::R1).unwrap(),
+            b.get_register(RegisterId::R0).unwrap().unwrap(),
+            b.value.into()
+        );
+        assert_eq!(
+            b.get_register(RegisterId::R1).unwrap().unwrap(),
             b.script_bytes().unwrap().into()
         );
         assert_eq!(
-            b.get_register(RegisterId::R2).unwrap(),
+            b.get_register(RegisterId::R2).unwrap().unwrap(),
             b.tokens_raw().into()
         );
         assert_eq!(
-            b.get_register(RegisterId::R3).unwrap(),
+            b.get_register(RegisterId::R3).unwrap().unwrap(),
             b.creation_info().into()
         );
     }
@@ -519,3 +520,4 @@ mod tests {
         }
     }
 }
+// += a + b
diff --git a/ergotree-ir/src/chain/ergo_box/register.rs b/ergotree-ir/src/chain/ergo_box/register.rs
index 7e62c2afb..503111b15 100644
--- a/ergotree-ir/src/chain/ergo_box/register.rs
+++ b/ergotree-ir/src/chain/ergo_box/register.rs
@@ -1,165 +1,23 @@
 //! Box registers
 
 use crate::mir::constant::Constant;
+use crate::mir::expr::Expr;
 use crate::serialization::sigma_byte_reader::SigmaByteRead;
 use crate::serialization::sigma_byte_writer::SigmaByteWrite;
 use crate::serialization::SigmaParsingError;
 use crate::serialization::SigmaSerializable;
 use crate::serialization::SigmaSerializationError;
 use crate::serialization::SigmaSerializeResult;
-use derive_more::From;
 use ergo_chain_types::Base16EncodedBytes;
 use std::convert::TryInto;
 use std::{collections::HashMap, convert::TryFrom};
 use thiserror::Error;
 
-/// Box register id (0-9)
-#[derive(PartialEq, Eq, Debug, Clone, Copy, From)]
-pub enum RegisterId {
-    /// Id for mandatory registers (0-3)
-    MandatoryRegisterId(MandatoryRegisterId),
-    /// Id for non-mandotory registers (4-9)
-    NonMandatoryRegisterId(NonMandatoryRegisterId),
-}
-
-impl RegisterId {
-    /// R0 register id (box.value)
-    pub const R0: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R0);
-    /// R1 register id (serialized ErgoTree)
-    pub const R1: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R1);
-    /// R2 register id (tokens)
-    pub const R2: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R2);
-    /// R2 register id (creationInfo)
-    pub const R3: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R3);
-}
-
-/// Register id out of bounds error (not in 0-9 range)
-#[derive(Error, PartialEq, Eq, Debug, Clone)]
-#[error("register id {0} is out of bounds (0 - 9)")]
-pub struct RegisterIdOutOfBounds(pub i8);
-
-impl TryFrom<i8> for RegisterId {
-    type Error = RegisterIdOutOfBounds;
-
-    fn try_from(value: i8) -> Result<Self, Self::Error> {
-        if value < 0 {
-            return Err(RegisterIdOutOfBounds(value));
-        }
-        let v = value as usize;
-        if v < NonMandatoryRegisterId::START_INDEX {
-            Ok(RegisterId::MandatoryRegisterId(value.try_into()?))
-        } else if v <= NonMandatoryRegisterId::END_INDEX {
-            Ok(RegisterId::NonMandatoryRegisterId(value.try_into()?))
-        } else {
-            Err(RegisterIdOutOfBounds(value))
-        }
-    }
-}
-
-impl TryFrom<u8> for RegisterId {
-    type Error = RegisterIdOutOfBounds;
-
-    fn try_from(value: u8) -> Result<Self, Self::Error> {
-        RegisterId::try_from(value as i8)
-    }
-}
-
-/// newtype for additional registers R4 - R9
-#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
-#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
-#[cfg_attr(feature = "json", serde(into = "String", try_from = "String"))]
-#[repr(u8)]
-pub enum NonMandatoryRegisterId {
-    /// id for R4 register
-    R4 = 4,
-    /// id for R5 register
-    R5 = 5,
-    /// id for R6 register
-    R6 = 6,
-    /// id for R7 register
-    R7 = 7,
-    /// id for R8 register
-    R8 = 8,
-    /// id for R9 register
-    R9 = 9,
-}
-
-impl NonMandatoryRegisterId {
-    /// starting index for non-mandatory registers
-    pub const START_INDEX: usize = 4;
-    /// end index for non-mandatory registers
-    pub const END_INDEX: usize = 9;
-
-    /// max number of registers
-    pub const NUM_REGS: usize = 6;
-
-    /// all register ids
-    pub const REG_IDS: [NonMandatoryRegisterId; NonMandatoryRegisterId::NUM_REGS] = [
-        NonMandatoryRegisterId::R4,
-        NonMandatoryRegisterId::R5,
-        NonMandatoryRegisterId::R6,
-        NonMandatoryRegisterId::R7,
-        NonMandatoryRegisterId::R8,
-        NonMandatoryRegisterId::R9,
-    ];
-
-    /// get register by it's index starting from 0
-    /// `i` is expected to be in range 0..[`Self::NUM_REGS`] , otherwise panic
-    pub fn get_by_zero_index(i: usize) -> NonMandatoryRegisterId {
-        assert!(i < NonMandatoryRegisterId::NUM_REGS);
-        NonMandatoryRegisterId::REG_IDS[i]
-    }
-}
+mod id;
+pub use id::*;
 
-impl From<NonMandatoryRegisterId> for String {
-    fn from(v: NonMandatoryRegisterId) -> Self {
-        format!("R{}", v as u8)
-    }
-}
-
-impl TryFrom<String> for NonMandatoryRegisterId {
-    type Error = NonMandatoryRegisterIdParsingError;
-    fn try_from(str: String) -> Result<Self, Self::Error> {
-        if str.len() == 2 && &str[..1] == "R" {
-            let index = str[1..2]
-                .parse::<usize>()
-                .map_err(|_| NonMandatoryRegisterIdParsingError())?;
-            if (NonMandatoryRegisterId::START_INDEX..=NonMandatoryRegisterId::END_INDEX)
-                .contains(&index)
-            {
-                Ok(NonMandatoryRegisterId::get_by_zero_index(
-                    index - NonMandatoryRegisterId::START_INDEX,
-                ))
-            } else {
-                Err(NonMandatoryRegisterIdParsingError())
-            }
-        } else {
-            Err(NonMandatoryRegisterIdParsingError())
-        }
-    }
-}
-
-impl TryFrom<i8> for NonMandatoryRegisterId {
-    type Error = RegisterIdOutOfBounds;
-
-    fn try_from(value: i8) -> Result<Self, Self::Error> {
-        let v_usize = value as usize;
-        if (NonMandatoryRegisterId::START_INDEX..=NonMandatoryRegisterId::END_INDEX)
-            .contains(&v_usize)
-        {
-            Ok(NonMandatoryRegisterId::get_by_zero_index(
-                v_usize - NonMandatoryRegisterId::START_INDEX,
-            ))
-        } else {
-            Err(RegisterIdOutOfBounds(value))
-        }
-    }
-}
-
-#[derive(Error, PartialEq, Eq, Debug, Clone)]
-#[error("failed to parse register id")]
-/// Error for failed parsing of the register id from string
-pub struct NonMandatoryRegisterIdParsingError();
+mod value;
+pub use value::*;
 
 /// Stores non-mandatory registers for the box
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -208,48 +66,21 @@ impl NonMandatoryRegisters {
         self.0.get(reg_id as usize)
     }
 
-    /// Get register value as a Constant (returns None, if there is no value for the given register id or if it's an unparseable)
-    pub fn get_constant(&self, reg_id: NonMandatoryRegisterId) -> Option<&Constant> {
-        self.0
+    /// Get register value as a Constant
+    /// returns None, if there is no value for the given register id or an error if it's an unparseable
+    pub fn get_constant(
+        &self,
+        reg_id: NonMandatoryRegisterId,
+    ) -> Result<Option<Constant>, RegisterValueError> {
+        match self
+            .0
             .get(reg_id as usize - NonMandatoryRegisterId::START_INDEX)
-            .and_then(|rv| rv.as_option_constant())
-    }
-}
-
-/// Register value (either Constant or bytes if it's unparseable)
-#[derive(PartialEq, Eq, Debug, Clone, From)]
-pub enum RegisterValue {
-    /// Constant value
-    Parsed(Constant),
-    /// Unparseable bytes
-    Unparseable {
-        /// Bytes that were not parsed (whole register bytes)
-        bytes: Vec<u8>,
-        /// Error message on parsing
-        error_msg: String,
-    },
-}
-
-impl RegisterValue {
-    /// Return a Constant if it's parsed, otherwise None
-    pub fn as_option_constant(&self) -> Option<&Constant> {
-        match self {
-            RegisterValue::Parsed(c) => Some(c),
-            RegisterValue::Unparseable {
-                bytes: _,
-                error_msg: _,
-            } => None,
-        }
-    }
-
-    #[allow(clippy::unwrap_used)] // it could only fail on OOM, etc.
-    fn sigma_serialize_bytes(&self) -> Vec<u8> {
-        match self {
-            RegisterValue::Parsed(c) => c.sigma_serialize_bytes().unwrap(),
-            RegisterValue::Unparseable {
-                bytes,
-                error_msg: _,
-            } => bytes.clone(),
+        {
+            Some(rv) => match rv.as_constant() {
+                Ok(c) => Ok(Some(c.clone())),
+                Err(e) => Err(e),
+            },
+            None => Ok(None),
         }
     }
 }
@@ -287,7 +118,8 @@ impl SigmaSerializable for NonMandatoryRegisters {
         for (idx, reg_value) in self.0.iter().enumerate() {
             match reg_value {
                 RegisterValue::Parsed(c) => c.sigma_serialize(w)?,
-                RegisterValue::Unparseable { bytes, error_msg } => {
+                RegisterValue::ParsedTupleExpr(t) => t.to_tuple_expr().sigma_serialize(w)?,
+                RegisterValue::Invalid { bytes, error_msg } => {
                     let bytes_str = base16::encode_lower(bytes);
                     return Err(SigmaSerializationError::NotSupported(format!("unparseable register value at {0:?} (parsing error: {error_msg}) cannot be serialized in the stream (writer), because it cannot be parsed later. Register value as base16-encoded bytes: {bytes_str}", NonMandatoryRegisterId::get_by_zero_index(idx))));
                 }
@@ -299,9 +131,27 @@ impl SigmaSerializable for NonMandatoryRegisters {
     fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
         let regs_num = r.get_u8()?;
         let mut additional_regs = Vec::with_capacity(regs_num as usize);
-        for _ in 0..regs_num {
-            let v = Constant::sigma_parse(r)?;
-            additional_regs.push(v);
+        for idx in 0..regs_num {
+            let expr = Expr::sigma_parse(r)?;
+            let reg_val = match expr {
+                Expr::Const(c) => RegisterValue::Parsed(c),
+                Expr::Tuple(t) => {
+                    RegisterValue::ParsedTupleExpr(EvaluatedTuple::new(t).map_err(|e| {
+                        RegisterValueError::UnexpectedRegisterValue(format!(
+                            "error parsing tuple expression from register {0:?}: {e}",
+                            RegisterId::try_from(idx)
+                        ))
+                    })?)
+                }
+                _ => {
+                    return Err(RegisterValueError::UnexpectedRegisterValue(format!(
+                        "invalid register ({0:?}) value: {expr:?} (expected Constant or Tuple)",
+                        RegisterId::try_from(idx)
+                    ))
+                    .into())
+                }
+            };
+            additional_regs.push(reg_val);
         }
         Ok(additional_regs.try_into()?)
     }
@@ -387,33 +237,6 @@ impl From<NonMandatoryRegistersError> for SigmaParsingError {
     }
 }
 
-/// Register ids that every box have (box properties exposed as registers)
-#[derive(PartialEq, Eq, Debug, Clone, Copy)]
-pub enum MandatoryRegisterId {
-    /// Monetary value, in Ergo tokens
-    R0 = 0,
-    /// Guarding script
-    R1 = 1,
-    /// Secondary tokens
-    R2 = 2,
-    /// Reference to transaction and output id where the box was created
-    R3 = 3,
-}
-
-impl TryFrom<i8> for MandatoryRegisterId {
-    type Error = RegisterIdOutOfBounds;
-
-    fn try_from(value: i8) -> Result<Self, Self::Error> {
-        match value {
-            v if v == MandatoryRegisterId::R0 as i8 => Ok(MandatoryRegisterId::R0),
-            v if v == MandatoryRegisterId::R1 as i8 => Ok(MandatoryRegisterId::R1),
-            v if v == MandatoryRegisterId::R2 as i8 => Ok(MandatoryRegisterId::R2),
-            v if v == MandatoryRegisterId::R3 as i8 => Ok(MandatoryRegisterId::R3),
-            _ => Err(RegisterIdOutOfBounds(value)),
-        }
-    }
-}
-
 #[allow(clippy::unwrap_used)]
 #[cfg(feature = "arbitrary")]
 pub(crate) mod arbitrary {
@@ -435,7 +258,7 @@ pub(crate) mod arbitrary {
                     prop_oneof![
                         any::<Constant>().prop_map(RegisterValue::Parsed),
                         vec(any::<u8>(), 0..100).prop_map({
-                            |bytes| RegisterValue::Unparseable {
+                            |bytes| RegisterValue::Invalid {
                                 bytes,
                                 error_msg: "unparseable".to_string(),
                             }
@@ -476,7 +299,7 @@ mod tests {
         fn get(regs in any::<NonMandatoryRegisters>()) {
             let hash_map: HashMap<NonMandatoryRegisterId, RegisterValue> = regs.clone().into();
             hash_map.keys().try_for_each(|reg_id| {
-                prop_assert_eq![regs.get_constant(*reg_id), hash_map.get(reg_id).unwrap().as_option_constant()];
+                prop_assert_eq![&regs.get_constant(*reg_id).unwrap().unwrap(), hash_map.get(reg_id).unwrap().as_constant().unwrap()];
                 Ok(())
             })?;
         }
diff --git a/ergotree-ir/src/chain/ergo_box/register/id.rs b/ergotree-ir/src/chain/ergo_box/register/id.rs
new file mode 100644
index 000000000..6fefa624e
--- /dev/null
+++ b/ergotree-ir/src/chain/ergo_box/register/id.rs
@@ -0,0 +1,177 @@
+use derive_more::From;
+use thiserror::Error;
+
+/// Box register id (0-9)
+#[derive(PartialEq, Eq, Debug, Clone, Copy, From)]
+pub enum RegisterId {
+    /// Id for mandatory registers (0-3)
+    MandatoryRegisterId(MandatoryRegisterId),
+    /// Id for non-mandotory registers (4-9)
+    NonMandatoryRegisterId(NonMandatoryRegisterId),
+}
+
+impl RegisterId {
+    /// R0 register id (box.value)
+    pub const R0: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R0);
+    /// R1 register id (serialized ErgoTree)
+    pub const R1: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R1);
+    /// R2 register id (tokens)
+    pub const R2: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R2);
+    /// R2 register id (creationInfo)
+    pub const R3: RegisterId = RegisterId::MandatoryRegisterId(MandatoryRegisterId::R3);
+}
+
+/// Register id out of bounds error (not in 0-9 range)
+#[derive(Error, PartialEq, Eq, Debug, Clone)]
+#[error("register id {0} is out of bounds (0 - 9)")]
+pub struct RegisterIdOutOfBounds(pub i8);
+
+impl TryFrom<i8> for RegisterId {
+    type Error = RegisterIdOutOfBounds;
+
+    fn try_from(value: i8) -> Result<Self, Self::Error> {
+        if value < 0 {
+            return Err(RegisterIdOutOfBounds(value));
+        }
+        let v = value as usize;
+        if v < NonMandatoryRegisterId::START_INDEX {
+            Ok(RegisterId::MandatoryRegisterId(value.try_into()?))
+        } else if v <= NonMandatoryRegisterId::END_INDEX {
+            Ok(RegisterId::NonMandatoryRegisterId(value.try_into()?))
+        } else {
+            Err(RegisterIdOutOfBounds(value))
+        }
+    }
+}
+
+impl TryFrom<u8> for RegisterId {
+    type Error = RegisterIdOutOfBounds;
+
+    fn try_from(value: u8) -> Result<Self, Self::Error> {
+        RegisterId::try_from(value as i8)
+    }
+}
+
+/// Register ids that every box have (box properties exposed as registers)
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub enum MandatoryRegisterId {
+    /// Monetary value, in Ergo tokens
+    R0 = 0,
+    /// Guarding script
+    R1 = 1,
+    /// Secondary tokens
+    R2 = 2,
+    /// Reference to transaction and output id where the box was created
+    R3 = 3,
+}
+
+impl TryFrom<i8> for MandatoryRegisterId {
+    type Error = RegisterIdOutOfBounds;
+
+    fn try_from(value: i8) -> Result<Self, Self::Error> {
+        match value {
+            v if v == MandatoryRegisterId::R0 as i8 => Ok(MandatoryRegisterId::R0),
+            v if v == MandatoryRegisterId::R1 as i8 => Ok(MandatoryRegisterId::R1),
+            v if v == MandatoryRegisterId::R2 as i8 => Ok(MandatoryRegisterId::R2),
+            v if v == MandatoryRegisterId::R3 as i8 => Ok(MandatoryRegisterId::R3),
+            _ => Err(RegisterIdOutOfBounds(value)),
+        }
+    }
+}
+
+/// newtype for additional registers R4 - R9
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
+#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "json", serde(into = "String", try_from = "String"))]
+#[repr(u8)]
+pub enum NonMandatoryRegisterId {
+    /// id for R4 register
+    R4 = 4,
+    /// id for R5 register
+    R5 = 5,
+    /// id for R6 register
+    R6 = 6,
+    /// id for R7 register
+    R7 = 7,
+    /// id for R8 register
+    R8 = 8,
+    /// id for R9 register
+    R9 = 9,
+}
+
+impl NonMandatoryRegisterId {
+    /// starting index for non-mandatory registers
+    pub const START_INDEX: usize = 4;
+    /// end index for non-mandatory registers
+    pub const END_INDEX: usize = 9;
+
+    /// max number of registers
+    pub const NUM_REGS: usize = 6;
+
+    /// all register ids
+    pub const REG_IDS: [NonMandatoryRegisterId; NonMandatoryRegisterId::NUM_REGS] = [
+        NonMandatoryRegisterId::R4,
+        NonMandatoryRegisterId::R5,
+        NonMandatoryRegisterId::R6,
+        NonMandatoryRegisterId::R7,
+        NonMandatoryRegisterId::R8,
+        NonMandatoryRegisterId::R9,
+    ];
+
+    /// get register by it's index starting from 0
+    /// `i` is expected to be in range 0..[`Self::NUM_REGS`] , otherwise panic
+    pub fn get_by_zero_index(i: usize) -> NonMandatoryRegisterId {
+        assert!(i < NonMandatoryRegisterId::NUM_REGS);
+        NonMandatoryRegisterId::REG_IDS[i]
+    }
+}
+
+impl From<NonMandatoryRegisterId> for String {
+    fn from(v: NonMandatoryRegisterId) -> Self {
+        format!("R{}", v as u8)
+    }
+}
+
+impl TryFrom<String> for NonMandatoryRegisterId {
+    type Error = NonMandatoryRegisterIdParsingError;
+    fn try_from(str: String) -> Result<Self, Self::Error> {
+        if str.len() == 2 && &str[..1] == "R" {
+            let index = str[1..2]
+                .parse::<usize>()
+                .map_err(|_| NonMandatoryRegisterIdParsingError())?;
+            if (NonMandatoryRegisterId::START_INDEX..=NonMandatoryRegisterId::END_INDEX)
+                .contains(&index)
+            {
+                Ok(NonMandatoryRegisterId::get_by_zero_index(
+                    index - NonMandatoryRegisterId::START_INDEX,
+                ))
+            } else {
+                Err(NonMandatoryRegisterIdParsingError())
+            }
+        } else {
+            Err(NonMandatoryRegisterIdParsingError())
+        }
+    }
+}
+
+impl TryFrom<i8> for NonMandatoryRegisterId {
+    type Error = RegisterIdOutOfBounds;
+
+    fn try_from(value: i8) -> Result<Self, Self::Error> {
+        let v_usize = value as usize;
+        if (NonMandatoryRegisterId::START_INDEX..=NonMandatoryRegisterId::END_INDEX)
+            .contains(&v_usize)
+        {
+            Ok(NonMandatoryRegisterId::get_by_zero_index(
+                v_usize - NonMandatoryRegisterId::START_INDEX,
+            ))
+        } else {
+            Err(RegisterIdOutOfBounds(value))
+        }
+    }
+}
+
+#[derive(Error, PartialEq, Eq, Debug, Clone)]
+#[error("failed to parse register id")]
+/// Error for failed parsing of the register id from string
+pub struct NonMandatoryRegisterIdParsingError();
diff --git a/ergotree-ir/src/chain/ergo_box/register/value.rs b/ergotree-ir/src/chain/ergo_box/register/value.rs
new file mode 100644
index 000000000..0a78f060e
--- /dev/null
+++ b/ergotree-ir/src/chain/ergo_box/register/value.rs
@@ -0,0 +1,172 @@
+use derive_more::From;
+use thiserror::Error;
+
+use crate::mir::constant::Constant;
+use crate::mir::constant::Literal;
+use crate::mir::expr::Expr;
+use crate::mir::tuple::Tuple;
+use crate::serialization::SigmaSerializable;
+
+/// Register value (either Constant or bytes if it's unparseable)
+#[derive(PartialEq, Eq, Debug, Clone, From)]
+pub enum RegisterValue {
+    /// Constant value
+    Parsed(Constant),
+    /// Parsed evaluated Tuple expression
+    /// see https://github.com/ergoplatform/sigma-rust/issues/700
+    ParsedTupleExpr(EvaluatedTuple),
+    /// Unparseable bytes
+    Invalid {
+        /// Bytes that were not parsed (whole register bytes)
+        bytes: Vec<u8>,
+        /// Error message on parsing
+        error_msg: String,
+    },
+}
+
+/// Ensures that tuple only contains Constant values
+/// see https://github.com/ergoplatform/sigma-rust/issues/700
+#[derive(PartialEq, Eq, Debug, Clone, From)]
+pub struct EvaluatedTuple {
+    tuple: Tuple,
+    constant: Constant,
+}
+
+impl EvaluatedTuple {
+    /// Create new EvaluatedTuple from Tuple, returns error if it contains non-Constant values
+    pub fn new(tuple: Tuple) -> Result<EvaluatedTuple, RegisterValueError> {
+        match tuple_to_constant(&tuple) {
+            Ok(constant) => Ok(EvaluatedTuple { tuple, constant }),
+            Err(e) => Err(RegisterValueError::InvalidTupleExpr(format!(
+                "tuple expr {tuple:?} contain non-constant items: {e}"
+            ))),
+        }
+    }
+
+    /// Get inner Tuple
+    pub fn to_tuple_expr(&self) -> Expr {
+        self.tuple.clone().into()
+    }
+
+    /// Convert to Constant
+    pub fn as_constant(&self) -> &Constant {
+        &self.constant
+    }
+}
+
+/// Errors on parsing register values
+#[derive(Error, Debug, Clone, PartialEq, Eq)]
+pub enum RegisterValueError {
+    /// Invalid register value
+    #[error("Invalid register value: {0}")]
+    Invalid(String),
+    /// Invalid Tuple expression in the parsed regiser value
+    #[error("Invalid Tuple expression in the parsed regiser value: {0}")]
+    InvalidTupleExpr(String),
+    /// Unexpected register value
+    #[error("Unexpected register value: {0}")]
+    UnexpectedRegisterValue(String),
+}
+
+impl RegisterValue {
+    /// Return a Constant if it's parsed, otherwise None
+    pub fn as_constant(&self) -> Result<&Constant, RegisterValueError> {
+        match self {
+            RegisterValue::Parsed(c) => Ok(c),
+            RegisterValue::ParsedTupleExpr(t) => Ok(t.as_constant()),
+            RegisterValue::Invalid {
+                bytes: _,
+                error_msg,
+            } => Err(RegisterValueError::Invalid(error_msg.to_string())),
+        }
+    }
+
+    /// Return a seraialized bytes of the register value
+    #[allow(clippy::unwrap_used)] // it could only fail on OOM, etc.
+    pub fn sigma_serialize_bytes(&self) -> Vec<u8> {
+        match self {
+            RegisterValue::Parsed(c) => c.sigma_serialize_bytes().unwrap(),
+            RegisterValue::ParsedTupleExpr(t) => t.to_tuple_expr().sigma_serialize_bytes().unwrap(),
+            RegisterValue::Invalid {
+                bytes,
+                error_msg: _,
+            } => bytes.clone(),
+        }
+    }
+
+    /// Parse bytes to RegisterValue
+    pub fn sigma_parse_bytes(bytes: &[u8]) -> Self {
+        if let Ok(expr) = Expr::sigma_parse_bytes(bytes) {
+            match expr {
+                Expr::Const(c) => RegisterValue::Parsed(c),
+                Expr::Tuple(t) => match EvaluatedTuple::new(t) {
+                    Ok(et) => RegisterValue::ParsedTupleExpr(et),
+                    Err(e) => RegisterValue::Invalid {
+                        bytes: bytes.to_vec(),
+                        error_msg: format!("invalid tuple in register value : {e:?}"),
+                    },
+                },
+                e => RegisterValue::Invalid {
+                    bytes: bytes.to_vec(),
+                    error_msg: format!(
+                        "Unexpected parsed register value: {e:?} from bytes {0:?}",
+                        bytes
+                    ),
+                },
+            }
+        } else {
+            RegisterValue::Invalid {
+                bytes: bytes.to_vec(),
+                error_msg: format!("failed to parse register value: {0:?}", bytes),
+            }
+        }
+    }
+}
+
+/// Convert evaluated Tuple expression to Constant
+/// see https://github.com/ergoplatform/sigma-rust/issues/700
+fn tuple_to_constant(t: &Tuple) -> Result<Constant, String> {
+    let values = t.items.try_mapped_ref(|tuple_item| match tuple_item {
+        Expr::Const(c) => Ok(c.v.clone()),
+        Expr::Tuple(t) => Ok(tuple_to_constant(t)?.v),
+        e => return Err(format!("Unexpected value in tuple: {e:?}")),
+    })?;
+    let v = Literal::Tup(values);
+    let c = Constant { tpe: t.tpe(), v };
+    Ok(c)
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+    use crate::types::stuple::STuple;
+    use crate::types::stype::SType;
+
+    use super::*;
+
+    #[test]
+    fn test_tuple_expr_i700() {
+        let tuple_expr_bytes_str = "860202660263";
+        let tuple_expr_bytes = base16::decode(tuple_expr_bytes_str).unwrap();
+        assert!(
+            Constant::sigma_parse_bytes(&tuple_expr_bytes).is_err(),
+            "constant cannot be parsed from tuple expr"
+        );
+        let reg_value = RegisterValue::sigma_parse_bytes(&tuple_expr_bytes);
+        // now let's construct a Constant for (102, 99) byte tuple
+        let expected_constant: Constant = Constant {
+            tpe: SType::STuple(STuple::pair(SType::SByte, SType::SByte)),
+            v: Literal::Tup([Literal::Byte(102), Literal::Byte(99)].into()),
+        };
+        assert_eq!(
+            reg_value.as_constant().unwrap(),
+            &expected_constant,
+            "should be accessible as Constant"
+        );
+        assert_eq!(
+            reg_value.sigma_serialize_bytes(),
+            tuple_expr_bytes,
+            "preserve tuple expr on serialization"
+        );
+    }
+}
diff --git a/ergotree-ir/src/chain/json/ergo_box.rs b/ergotree-ir/src/chain/json/ergo_box.rs
index 5727afce6..d42d19a81 100644
--- a/ergotree-ir/src/chain/json/ergo_box.rs
+++ b/ergotree-ir/src/chain/json/ergo_box.rs
@@ -8,9 +8,7 @@ use crate::chain::ergo_box::RegisterValue;
 use crate::chain::token::Token;
 use crate::chain::tx_id::TxId;
 use crate::ergo_tree::ErgoTree;
-use crate::mir::constant::Constant;
 use crate::serialization::SigmaParsingError;
-use crate::serialization::SigmaSerializable;
 use crate::serialization::SigmaSerializationError;
 use ergo_chain_types::Base16DecodedBytes;
 use std::convert::TryFrom;
@@ -200,13 +198,7 @@ pub struct ConstantHolder(#[serde(deserialize_with = "super::t_as_string_or_stru
 
 impl From<ConstantHolder> for RegisterValue {
     fn from(ch: ConstantHolder) -> Self {
-        match Constant::sigma_parse_bytes(ch.0.raw_value.0.as_slice()) {
-            Ok(c) => RegisterValue::Parsed(c),
-            Err(e) => RegisterValue::Unparseable {
-                bytes: ch.0.raw_value.0,
-                error_msg: format!("{e}"),
-            },
-        }
+        RegisterValue::sigma_parse_bytes(ch.0.raw_value.0.as_slice())
     }
 }
 
@@ -286,7 +278,7 @@ mod tests {
             {"R4":{"serializedValue":"0500","sigmaType":"SLong","renderedValue":"0"}}
                                                                                                                                            "#;
         let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
-        assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_some());
+        assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_ok());
     }
 
     #[test]
@@ -301,7 +293,7 @@ mod tests {
             }
         "#;
         let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
-        assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_some());
+        assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_ok());
     }
 
     #[test]
@@ -314,16 +306,6 @@ mod tests {
         assert!(regs.is_err());
     }
 
-    #[test]
-    fn parse_registers_error2() {
-        // invalid uparseable constant value
-        let json = r#"
-            {"R4":"860202660263"}
-        "#;
-        let regs: Result<NonMandatoryRegisters, _> = serde_json::from_str(json);
-        assert!(regs.is_ok());
-    }
-
     #[test]
     fn parse_registers_unit() {
         let json = r#" {"R4":"62"} "#;
diff --git a/ergotree-ir/src/serialization/serializable.rs b/ergotree-ir/src/serialization/serializable.rs
index b5c6ddea9..e51dec4ac 100644
--- a/ergotree-ir/src/serialization/serializable.rs
+++ b/ergotree-ir/src/serialization/serializable.rs
@@ -1,4 +1,5 @@
 //! Serialization of Ergo types
+use crate::chain::ergo_box::RegisterValueError;
 use crate::ergo_tree::ErgoTreeHeaderError;
 use crate::mir::val_def::ValId;
 use crate::mir::{constant::TryExtractFromError, expr::InvalidArgumentError};
@@ -54,7 +55,7 @@ pub enum SigmaParsingError {
     #[error("not implemented op error: {0}")]
     NotImplementedOpCode(String),
     /// Failed to parse type
-    #[error("type parsing error, invalid type code: {0}")]
+    #[error("type parsing error, invalid type code: {0}({0:#04X})")]
     InvalidTypeCode(u8),
     /// Failed to decode VLQ
     #[error("vlq encode error: {0}")]
@@ -101,6 +102,9 @@ pub enum SigmaParsingError {
     /// ErgoTreeHeaderError
     #[error("ErgoTreeHeaderError: {0}")]
     ErgoTreeHeaderError(#[from] ErgoTreeHeaderError),
+    /// Invalid register value
+    #[error("Invalid register value: {0}")]
+    InvalidRegisterValue(#[from] RegisterValueError),
 }
 
 impl From<io::Error> for SigmaParsingError {
diff --git a/sigma-ser/src/scorex_serialize.rs b/sigma-ser/src/scorex_serialize.rs
index 3058a701b..f43ed2874 100644
--- a/sigma-ser/src/scorex_serialize.rs
+++ b/sigma-ser/src/scorex_serialize.rs
@@ -48,7 +48,7 @@ pub enum ScorexParsingError {
     #[error("not implemented op error")]
     NotImplementedOpCode(String),
     /// Failed to parse type
-    #[error("type parsing error")]
+    #[error("type parsing error, invalid type code: {0}({0:#04X})")]
     InvalidTypeCode(u8),
     /// Failed to decode VLQ
     #[error("vlq encode error: {0}")]


Test patch: