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

Issue:
Can't parse mainnet tx with SSTring
https://ergexplorer.com/transactions#97132a4a14691638b73bb542e6bcbb847f1d3c93ffdbf82796488dc10cb5f30f

This TX fails deserialization because of SSTring in script of output box #0. Compare sigma-rust and ergo/sigmastate-interpreter's behavior. Based on my testing seems sigmastate-interpreter can parse this output tree fine.


Discussion / hints:

Fix patch:
diff --git a/ergotree-ir/src/mir/constant.rs b/ergotree-ir/src/mir/constant.rs
index f07107bbd..c6c705fe8 100644
--- a/ergotree-ir/src/mir/constant.rs
+++ b/ergotree-ir/src/mir/constant.rs
@@ -70,6 +70,8 @@ pub enum Literal {
     Int(i32),
     /// Long
     Long(i64),
+    /// String type
+    String(Arc<str>),
     /// Big integer
     BigInt(BigInt256),
     /// Sigma property
@@ -120,6 +122,7 @@ impl core::fmt::Debug for Literal {
             Literal::GroupElement(v) => v.fmt(f),
             Literal::AvlTree(v) => v.fmt(f),
             Literal::CBox(v) => v.fmt(f),
+            Literal::String(v) => v.fmt(f),
         }
     }
 }
@@ -177,6 +180,7 @@ impl core::fmt::Display for Literal {
             Literal::GroupElement(v) => v.fmt(f),
             Literal::AvlTree(v) => write!(f, "AvlTree({:?})", v),
             Literal::CBox(v) => write!(f, "ErgoBox({:?})", v),
+            Literal::String(v) => write!(f, "String({v})"),
         }
     }
 }
@@ -366,6 +370,7 @@ impl<'ctx> TryFrom<Value<'ctx>> for Constant {
                 }
             }
             Value::AvlTree(a) => Ok(Constant::from(*a)),
+            Value::String(s) => Ok(Constant::from(s)),
             Value::Context => Err("Cannot convert Value::Context into Constant".into()),
             Value::Header(_) => Err("Cannot convert Value::Header(_) into Constant".into()),
             Value::PreHeader(_) => Err("Cannot convert Value::PreHeader(_) into Constant".into()),
@@ -597,6 +602,24 @@ impl From<ADDigest> for Constant {
     }
 }
 
+impl From<Arc<str>> for Constant {
+    fn from(s: Arc<str>) -> Self {
+        Constant {
+            tpe: SType::SString,
+            v: Literal::String(s),
+        }
+    }
+}
+
+impl From<String> for Constant {
+    fn from(s: String) -> Self {
+        Constant {
+            tpe: SType::SString,
+            v: Literal::String(s.into()),
+        }
+    }
+}
+
 #[allow(clippy::unwrap_used)]
 #[allow(clippy::from_over_into)]
 #[impl_for_tuples(2, 4)]
@@ -835,6 +858,19 @@ impl TryExtractFrom<Literal> for BigInt256 {
     }
 }
 
+impl TryExtractFrom<Literal> for String {
+    fn try_extract_from(v: Literal) -> Result<Self, TryExtractFromError> {
+        match v {
+            Literal::String(s) => Ok(String::from(&*s)),
+            _ => Err(TryExtractFromError(format!(
+                "expected {:?}, found {:?}",
+                core::any::type_name::<Self>(),
+                v
+            ))),
+        }
+    }
+}
+
 impl TryExtractFrom<Literal> for AvlTreeData {
     fn try_extract_from(v: Literal) -> Result<Self, TryExtractFromError> {
         match v {
@@ -1124,6 +1160,19 @@ pub mod tests {
         test_constant_roundtrip(());
     }
 
+    // test that invalid strings don't error but are instead parsed lossily to match reference impl
+    #[test]
+    fn parse_invalid_string() {
+        let mut bytes = Constant::from(".".to_string())
+            .sigma_serialize_bytes()
+            .unwrap();
+        *bytes.last_mut().unwrap() = 0xf0;
+        assert_eq!(
+            Constant::sigma_parse_bytes(&bytes).unwrap().v,
+            Literal::String("�".into())
+        );
+    }
+
     proptest! {
 
         #![proptest_config(ProptestConfig::with_cases(8))]
@@ -1256,5 +1305,10 @@ pub mod tests {
             test_constant_roundtrip(v);
         }
 
+        #[test]
+        fn string_roundtrip(v in any::<String>()) {
+            test_constant_roundtrip(v);
+        }
+
     }
 }
diff --git a/ergotree-ir/src/mir/value.rs b/ergotree-ir/src/mir/value.rs
index 9991bb703..1783226ed 100644
--- a/ergotree-ir/src/mir/value.rs
+++ b/ergotree-ir/src/mir/value.rs
@@ -198,6 +198,8 @@ pub enum Value<'ctx> {
     Tup(TupleItems<Value<'ctx>>),
     /// Transaction(and blockchain) context info
     Context,
+    /// String type
+    String(Arc<str>),
     /// Block header
     Header(Box<Header>),
     /// Header with predictable data
@@ -242,6 +244,7 @@ impl<'ctx> Value<'ctx> {
                     .unwrap(),
             ),
             Value::Context => Value::Context,
+            Value::String(s) => Value::String(s.clone()),
             Value::Header(h) => Value::Header(h.clone()),
             Value::PreHeader(h) => Value::PreHeader(h.clone()),
             Value::Global => Value::Global,
@@ -312,6 +315,7 @@ impl From<Literal> for Value<'static> {
             Literal::Int(i) => Value::Int(i),
             Literal::Long(l) => Value::Long(l),
             Literal::BigInt(b) => Value::BigInt(b),
+            Literal::String(s) => Value::String(s),
             Literal::Unit => Value::Unit,
             Literal::SigmaProp(s) => Value::SigmaProp(s),
             Literal::GroupElement(e) => Value::GroupElement(e.into()),
@@ -382,6 +386,7 @@ impl core::fmt::Display for Value<'_> {
             Value::Int(v) => v.fmt(f),
             Value::Long(v) => write!(f, "{}L", v),
             Value::BigInt(v) => v.fmt(f),
+            Value::String(v) => v.fmt(f),
             Value::SigmaProp(v) => v.fmt(f),
             Value::GroupElement(v) => v.fmt(f),
             Value::AvlTree(v) => write!(f, "AvlTree({:?})", v),
@@ -405,6 +410,7 @@ impl StoreWrapped for i64 {}
 impl StoreWrapped for BigInt256 {}
 impl StoreWrapped for Header {}
 impl StoreWrapped for ErgoBox {}
+impl StoreWrapped for Arc<str> {}
 impl StoreWrapped for Ref<'_, ErgoBox> {}
 impl StoreWrapped for EcPoint {}
 impl StoreWrapped for SigmaProp {}
diff --git a/ergotree-ir/src/serialization/data.rs b/ergotree-ir/src/serialization/data.rs
index 50943e19b..ee6279639 100644
--- a/ergotree-ir/src/serialization/data.rs
+++ b/ergotree-ir/src/serialization/data.rs
@@ -1,5 +1,6 @@
 use alloc::boxed::Box;
 
+use alloc::string::String;
 use alloc::string::ToString;
 use alloc::vec;
 use alloc::vec::Vec;
@@ -44,6 +45,10 @@ impl DataSerializer {
             Literal::BigInt(v) => {
                 v.sigma_serialize(w)?;
             }
+            Literal::String(s) => {
+                w.put_usize_as_u32_unwrapped(s.len())?;
+                w.write_all(s.as_bytes())?;
+            }
             Literal::GroupElement(ecp) => ecp.sigma_serialize(w)?,
             Literal::SigmaProp(s) => s.value().sigma_serialize(w)?,
             Literal::AvlTree(a) => a.sigma_serialize(w)?,
@@ -101,6 +106,12 @@ impl DataSerializer {
             SShort => Literal::Short(r.get_i16()?),
             SInt => Literal::Int(r.get_i32()?),
             SLong => Literal::Long(r.get_i64()?),
+            SString => {
+                let len = r.get_u32()?;
+                let mut buf = vec![0; len as usize];
+                r.read_exact(&mut buf)?;
+                Literal::String(String::from_utf8_lossy(&buf).into())
+            }
             SBigInt => Literal::BigInt(BigInt256::sigma_parse(r)?),
             SUnit => Literal::Unit,
             SGroupElement => Literal::GroupElement(Arc::new(EcPoint::sigma_parse(r)?)),
diff --git a/ergotree-ir/src/serialization/types.rs b/ergotree-ir/src/serialization/types.rs
index 850a2ff5f..6e5ba5ad7 100644
--- a/ergotree-ir/src/serialization/types.rs
+++ b/ergotree-ir/src/serialization/types.rs
@@ -106,7 +106,7 @@ pub enum TypeCode {
     SBOX = 99,
     SAVL_TREE = 100,
     SCONTEXT = 101,
-    // SSTRING = 102,
+    SSTRING = 102,
     STYPE_VAR = 103,
     SHEADER = 104,
     SPRE_HEADER = 105,
@@ -322,6 +322,7 @@ impl SType {
             TypeCode::SBOX => SBox,
             TypeCode::SAVL_TREE => SAvlTree,
             TypeCode::SCONTEXT => SContext,
+            TypeCode::SSTRING => SString,
             TypeCode::STYPE_VAR => STypeVar(stype_param::STypeVar::sigma_parse(r)?),
             TypeCode::SHEADER => SHeader,
             TypeCode::SPRE_HEADER => SPreHeader,
@@ -362,6 +363,7 @@ impl SigmaSerializable for SType {
             SType::SBox => TypeCode::SBOX.sigma_serialize(w),
             SType::SAvlTree => TypeCode::SAVL_TREE.sigma_serialize(w),
             SType::SContext => TypeCode::SCONTEXT.sigma_serialize(w),
+            SType::SString => TypeCode::SSTRING.sigma_serialize(w),
             SType::SHeader => TypeCode::SHEADER.sigma_serialize(w),
             SType::SPreHeader => TypeCode::SPRE_HEADER.sigma_serialize(w),
             SType::SGlobal => TypeCode::SGLOBAL.sigma_serialize(w),
@@ -384,7 +386,8 @@ impl SigmaSerializable for SType {
                     SGroupElement => TypeCode::OPTION_COLL_GROUP_ELEMENT.sigma_serialize(w),
                     SSigmaProp => TypeCode::OPTION_COLL_SIGMAPROP.sigma_serialize(w),
                     STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_)
-                    | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => {
+                    | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader
+                    | SGlobal => {
                         // if not "embeddable" type fallback to generic Option type code following
                         // elem type code
                         TypeCode::OPTION.sigma_serialize(w)?;
@@ -392,7 +395,7 @@ impl SigmaSerializable for SType {
                     }
                 },
                 STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | STuple(_)
-                | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => {
+                | SFunc(_) | SContext | SString | SHeader | SPreHeader | SGlobal => {
                     // if not "embeddable" type fallback to generic Option type code following
                     // elem type code
                     TypeCode::OPTION.sigma_serialize(w)?;
@@ -419,7 +422,8 @@ impl SigmaSerializable for SType {
                     SGroupElement => TypeCode::NESTED_COLL_GROUP_ELEMENT.sigma_serialize(w),
                     SSigmaProp => TypeCode::NESTED_COLL_SIGMAPROP.sigma_serialize(w),
                     STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_)
-                    | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => {
+                    | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader
+                    | SGlobal => {
                         // if not "embeddable" type fallback to generic Coll type code following
                         // elem type code
                         TypeCode::COLL.sigma_serialize(w)?;
@@ -427,7 +431,7 @@ impl SigmaSerializable for SType {
                     }
                 },
                 STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | STuple(_)
-                | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => {
+                | SFunc(_) | SContext | SString | SHeader | SPreHeader | SGlobal => {
                     // if not "embeddable" type fallback to generic Coll type code following
                     // elem type code
                     TypeCode::COLL.sigma_serialize(w)?;
@@ -515,9 +519,11 @@ impl SigmaSerializable for SType {
                     }
                     (
                         STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_)
-                        | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal,
+                        | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader
+                        | SGlobal,
                         STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_)
-                        | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal,
+                        | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader
+                        | SGlobal,
                     ) => {
                         // Pair of non-primitive types (`(SBox, SAvlTree)`, `((Int, Byte), (Boolean,Box))`, etc.)
                         TypeCode::TUPLE_PAIR1.sigma_serialize(w)?;
diff --git a/ergotree-ir/src/types/stype.rs b/ergotree-ir/src/types/stype.rs
index f73ffc0d2..31bb7744a 100644
--- a/ergotree-ir/src/types/stype.rs
+++ b/ergotree-ir/src/types/stype.rs
@@ -1,5 +1,6 @@
 //! SType hierarchy
 
+use alloc::string::String;
 use alloc::sync::Arc;
 use alloc::vec::Vec;
 use core::convert::TryInto;
@@ -60,6 +61,8 @@ pub enum SType {
     SFunc(SFunc),
     /// Context object ("CONTEXT" in ErgoScript)
     SContext,
+    /// UTF-8 String type
+    SString,
     /// Header of a block
     SHeader,
     /// Header of a block without solved mining puzzle
@@ -92,6 +95,7 @@ impl SType {
                 | SType::SBox
                 | SType::SAvlTree
                 | SType::SContext
+                | SType::SString
                 | SType::SBoolean
                 | SType::SHeader
                 | SType::SPreHeader
@@ -150,6 +154,7 @@ impl core::fmt::Display for SType {
             SType::STuple(t) => write!(f, "{}", t),
             SType::SFunc(t) => write!(f, "{}", t),
             SType::SContext => write!(f, "Context"),
+            SType::SString => write!(f, "String"),
             SType::SHeader => write!(f, "Header"),
             SType::SPreHeader => write!(f, "PreHeader"),
             SType::SGlobal => write!(f, "Global"),
@@ -259,6 +264,12 @@ impl LiftIntoSType for AvlTreeData {
     }
 }
 
+impl LiftIntoSType for String {
+    fn stype() -> SType {
+        SType::SString
+    }
+}
+
 impl<T: LiftIntoSType> LiftIntoSType for Option<T> {
     fn stype() -> SType {
         SType::SOption(Arc::new(T::stype()))
@@ -295,6 +306,7 @@ pub(crate) mod tests {
             Just(SType::SBox),
             Just(SType::SAvlTree),
             Just(SType::SContext),
+            Just(SType::SString),
             Just(SType::SHeader),
             Just(SType::SPreHeader),
             Just(SType::SGlobal),


Test patch: