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/ergo-lib/src/chain/json/transaction.rs b/ergo-lib/src/chain/json/transaction.rs
index 7558613cf..a88dcd968 100644
--- a/ergo-lib/src/chain/json/transaction.rs
+++ b/ergo-lib/src/chain/json/transaction.rs
@@ -57,7 +57,7 @@ impl TryFrom<UnsignedTransactionJson> for UnsignedTransaction {
     type Error = String;
     fn try_from(tx_json: UnsignedTransactionJson) -> Result<Self, Self::Error> {
         UnsignedTransaction::new_from_vec(tx_json.inputs, tx_json.data_inputs, tx_json.outputs)
-            .map_err(|e| format!("TryFrom<UnsignedTransactionJson> error: {0}", e))
+            .map_err(|e| format!("TryFrom<UnsignedTransactionJson> error: {0:?}", e))
     }
 }
 
@@ -106,7 +106,7 @@ impl TryFrom<TransactionJson> for Transaction {
 }
 
 #[cfg(test)]
-#[allow(clippy::panic)]
+#[allow(clippy::panic, clippy::unwrap_used)]
 mod tests {
     use crate::chain::transaction::unsigned::UnsignedTransaction;
     use crate::chain::transaction::Transaction;
diff --git a/ergo-lib/src/chain/transaction/input.rs b/ergo-lib/src/chain/transaction/input.rs
index bbd52c980..d854f7f7f 100644
--- a/ergo-lib/src/chain/transaction/input.rs
+++ b/ergo-lib/src/chain/transaction/input.rs
@@ -59,6 +59,17 @@ impl UnsignedInput {
     pub fn new(box_id: BoxId, extension: ContextExtension) -> Self {
         UnsignedInput { box_id, extension }
     }
+
+    /// Create new Input with empty proof (for UnsignedTransaction id calculation)
+    pub fn input_to_sign(&self) -> Input {
+        Input {
+            box_id: self.box_id,
+            spending_proof: ProverResult {
+                proof: ProofBytes::Empty,
+                extension: self.extension.clone(),
+            },
+        }
+    }
 }
 
 impl<T: ErgoBoxId> From<T> for UnsignedInput {
diff --git a/ergo-lib/src/chain/transaction/unsigned.rs b/ergo-lib/src/chain/transaction/unsigned.rs
index c47e0054a..fbd298b3d 100644
--- a/ergo-lib/src/chain/transaction/unsigned.rs
+++ b/ergo-lib/src/chain/transaction/unsigned.rs
@@ -1,14 +1,14 @@
 //! Unsigned (without proofs) transaction
 
-use super::input::{Input, UnsignedInput};
-use super::prover_result::ProverResult;
+use super::input::UnsignedInput;
+
 use super::DataInput;
 use super::Transaction;
 use super::TxIoVec;
 use super::{distinct_token_ids, TransactionError};
 use bounded_vec::BoundedVec;
 use ergo_chain_types::blake2b256_hash;
-use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
+
 use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
 use ergotree_ir::chain::token::TokenId;
 use ergotree_ir::chain::tx_id::TxId;
@@ -81,26 +81,13 @@ impl UnsignedTransaction {
         Ok(TxId(blake2b256_hash(&bytes)))
     }
 
-    fn to_tx_without_proofs(&self) -> Transaction {
-        let empty_proofs_input = self.inputs.mapped_ref(|ui| {
-            Input::new(
-                ui.box_id,
-                ProverResult {
-                    proof: ProofBytes::Empty,
-                    extension: ui.extension.clone(),
-                },
-            )
-        });
-
-        #[allow(clippy::unwrap_used)]
-        // safe since the serialization error is impossible here
-        // since we already serialized this unsigned tx (on calc tx id)
+    fn to_tx_without_proofs(&self) -> Result<Transaction, SigmaSerializationError> {
+        let empty_proofs_input = self.inputs.mapped_ref(|ui| ui.input_to_sign());
         Transaction::new(
             empty_proofs_input,
             self.data_inputs.clone(),
             self.output_candidates.clone(),
         )
-        .unwrap()
     }
 
     /// Get transaction id
@@ -110,7 +97,7 @@ impl UnsignedTransaction {
 
     /// message to be signed by the [`ergotree_interpreter::sigma_protocol::prover::Prover`] (serialized tx)
     pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
-        let tx = self.to_tx_without_proofs();
+        let tx = self.to_tx_without_proofs()?;
         tx.bytes_to_sign()
     }
 
diff --git a/ergotree-ir/src/chain/ergo_box/box_value.rs b/ergotree-ir/src/chain/ergo_box/box_value.rs
index b487f7ffa..77927b0d5 100644
--- a/ergotree-ir/src/chain/ergo_box/box_value.rs
+++ b/ergotree-ir/src/chain/ergo_box/box_value.rs
@@ -184,13 +184,13 @@ pub enum BoxValueError {
 
 impl From<BoxValueError> for SigmaParsingError {
     fn from(e: BoxValueError) -> Self {
-        SigmaParsingError::ValueOutOfBounds(format!("{}", e))
+        SigmaParsingError::ValueOutOfBounds(format!("BoxValue error: {}", e))
     }
 }
 
 impl From<TokenAmountError> for SigmaParsingError {
     fn from(e: TokenAmountError) -> Self {
-        SigmaParsingError::ValueOutOfBounds(format!("{}", e))
+        SigmaParsingError::ValueOutOfBounds(format!("TokenAmount error: {}", e))
     }
 }
 
diff --git a/ergotree-ir/src/chain/ergo_box/register.rs b/ergotree-ir/src/chain/ergo_box/register.rs
index d96825eb0..7e62c2afb 100644
--- a/ergotree-ir/src/chain/ergo_box/register.rs
+++ b/ergotree-ir/src/chain/ergo_box/register.rs
@@ -222,7 +222,12 @@ pub enum RegisterValue {
     /// Constant value
     Parsed(Constant),
     /// Unparseable bytes
-    Unparseable(Vec<u8>),
+    Unparseable {
+        /// Bytes that were not parsed (whole register bytes)
+        bytes: Vec<u8>,
+        /// Error message on parsing
+        error_msg: String,
+    },
 }
 
 impl RegisterValue {
@@ -230,7 +235,10 @@ impl RegisterValue {
     pub fn as_option_constant(&self) -> Option<&Constant> {
         match self {
             RegisterValue::Parsed(c) => Some(c),
-            RegisterValue::Unparseable(_) => None,
+            RegisterValue::Unparseable {
+                bytes: _,
+                error_msg: _,
+            } => None,
         }
     }
 
@@ -238,7 +246,10 @@ impl RegisterValue {
     fn sigma_serialize_bytes(&self) -> Vec<u8> {
         match self {
             RegisterValue::Parsed(c) => c.sigma_serialize_bytes().unwrap(),
-            RegisterValue::Unparseable(bytes) => bytes.clone(),
+            RegisterValue::Unparseable {
+                bytes,
+                error_msg: _,
+            } => bytes.clone(),
         }
     }
 }
@@ -273,13 +284,12 @@ impl SigmaSerializable for NonMandatoryRegisters {
     fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
         let regs_num = self.len();
         w.put_u8(regs_num as u8)?;
-        for reg_value in self.0.iter() {
+        for (idx, reg_value) in self.0.iter().enumerate() {
             match reg_value {
                 RegisterValue::Parsed(c) => c.sigma_serialize(w)?,
-                RegisterValue::Unparseable(_) => {
-                    return Err(SigmaSerializationError::NotSupported(
-                        "unparseable register value cannot be serialized, because it cannot be parsed later"
-                    ))
+                RegisterValue::Unparseable { 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))));
                 }
             };
         }
@@ -424,7 +434,12 @@ pub(crate) mod arbitrary {
                 if params.allow_unparseable {
                     prop_oneof![
                         any::<Constant>().prop_map(RegisterValue::Parsed),
-                        vec(any::<u8>(), 0..100).prop_map(RegisterValue::Unparseable)
+                        vec(any::<u8>(), 0..100).prop_map({
+                            |bytes| RegisterValue::Unparseable {
+                                bytes,
+                                error_msg: "unparseable".to_string(),
+                            }
+                        })
                     ]
                     .boxed()
                 } else {
diff --git a/ergotree-ir/src/chain/json/ergo_box.rs b/ergotree-ir/src/chain/json/ergo_box.rs
index 9e8a29433..5727afce6 100644
--- a/ergotree-ir/src/chain/json/ergo_box.rs
+++ b/ergotree-ir/src/chain/json/ergo_box.rs
@@ -62,7 +62,8 @@ impl TryFrom<ErgoBoxJson> for ErgoBox {
         } else {
             Some(box_json.tokens.try_into().map_err(|_| {
                 SigmaSerializationError::NotSupported(
-                    "More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box",
+                    "More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box"
+                        .to_string(),
                 )
             })?)
         };
@@ -166,7 +167,8 @@ impl TryFrom<ErgoBoxCandidateJson> for ErgoBoxCandidate {
         } else {
             Some(box_json.tokens.try_into().map_err(|_| {
                 SigmaSerializationError::NotSupported(
-                    "More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box",
+                    "More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box"
+                        .to_string(),
                 )
             })?)
         };
@@ -200,7 +202,10 @@ 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(_) => RegisterValue::Unparseable(ch.0.raw_value.0),
+            Err(e) => RegisterValue::Unparseable {
+                bytes: ch.0.raw_value.0,
+                error_msg: format!("{e}"),
+            },
         }
     }
 }
diff --git a/ergotree-ir/src/serialization/constant.rs b/ergotree-ir/src/serialization/constant.rs
index 0dbfea98e..61ab8a3a2 100644
--- a/ergotree-ir/src/serialization/constant.rs
+++ b/ergotree-ir/src/serialization/constant.rs
@@ -68,4 +68,14 @@ mod tests {
         assert!(c_res.is_ok());
         assert_eq!(c_res.unwrap().tpe, SType::SColl(Box::new(SType::SBox)));
     }
+
+    #[test]
+    fn test_parse_r5_in_698() {
+        let constant_bytes_str = "0c63028092f401104904000e200137c91882b759ad46a20e39aa4d035ce32525dc76d021ee643e71d09446400f04020e20f6ff8b7210015545d4b3ac5fc60c908092d035a1a16155c029e8d511627c7a2c0e20efc4f603dea6041286a89f5bd516ac96ea5b25da4f08d76c6927e01d61b22adf040204000402040004000402040c044c04010404040404020e20f5918eb4b0283c669bdd8a195640766c19e40a693a6697b775b08e09052523d40e20767caa80b98e496ad8a9f689c4410ae453327f0f95e95084c0ae206350793b7704000402040004020412040005809bee0204000400040004000402040404000402041205d00f040304000402040204420580897a0e20012aec95af24812a01775de090411ba70a648fe859013f896ca2a1a95882ce5f040204040400041004100402041005000402040004100400040004000400040004100410040204100402040205000404040404020402040404040100d80dd601db6501fed602b27201730000d6037301d604b27201730200d605dc640be4c6720204640283020e73037304e4e3000ed606e4c6a70410d607b27206730500d608b2a5730600d609e4c672080410d60ab27209730700d60be3044005d60ce4720bd60d8c720c01d196830301938cb2db63087202730800017203938cb2db6308720473090001b4e4b27205730a00730b730c95ed947207720a937207730dd80cd60eb27201730e00d60fdb6308a7d610e4c6a70511d611720bd612720cd613b47210730fb17210d6148c721202d615b2a5731000d616dc640be4c6720e04640283020e73117312e4e3010ed617b2db63087215731300d6188cb2720f73140002d6197cb4e4b272167315007316731796830401938cb2db6308720e7318000172039683080193c27208c2a792c1720899c1a7731993b2db63087208731a00b2720f731b0093b27209731c00b27206731d0093e4c672080511721093e4c672080664e4c6a7066493720a9591b27210731e009d9cb2e4c672040511731f007cb4e4b27205732000732173227323720d7324edafdb0c0e7213d9011a049593721a720d93b27213721a00721490b27213721a00721491b17213720d91db6903db6503feb272107325009683040193cbc27215b4e4b272167326007327732892c172157329938c721701732a928c72170295927218721972187219d802d60ee4c6a70511d60fe4c6720805119594720e720fd809d610b2a4732b00d611e4c6b2a4732c00050ed612adb4db0c0e7211732d9db17211732ed90112047cb472119c7212732f9c9a721273307331d613b072127332d90113599a8c7213018c721302d614e4c6a70664d615e4c67210050ed616dc640a7214027215e4e3010ed617e67216d618e4e3020e96830801927cb4e4dc640ae4c672040464028cb2db6308721073330001e4e3030e73347335721393c27208c2a792c17208c1a793b2db63087208733600b2db6308a7733700937209720693b2720f733800b2720e733900957217d802d619e47216d61aadb4db0c0e7219733a9db17219733bd9011a047cb472199c721a733c9c9a721a733d733e9683020193b2720f733f009a99b2720e734000b0721a7341d9011b599a8c721b018c721b02721393b4720f7342b1720faddc0c1db4720e7343b1720e01addc0c1d721a017212d9011b59998c721b028c721b01d9011b599a8c721b018c721b029683020193b2720f7344009ab2720e734500721393b4720f7346b1720faddc0c1db4720e7347b1720e017212d90119599a8c7219018c72190293db6401e4c672080664db6401957217e4dc640d72140283013c0e0e8602721572117218e4dc640c72140283013c0e0e86027215721172187348e3893c02010b4858ce0425ed4748d0d3a59f2dbf874166a2caaf734655ac5e3f88a68cdd01012aec95af24812a01775de090411ba70a648fe859013f896ca2a1a95882ce5f904e0310020401110400000000644ec61f485b98eb87153f7c57db4f5ecd75556fddbc403b41acf8441fde8e160900072000d35f8400db49e16a8185956c1fce96819bd407f8597a65120fb6bc02ebbc7f5e00c0843d10230400040204000402040604040500050004000e200137c91882b759ad46a20e39aa4d035ce32525dc76d021ee643e71d09446400f04000e20010b4858ce0425ed4748d0d3a59f2dbf874166a2caaf734655ac5e3f88a68cdd0400040204080400040204040502040604080400040004020402040004020e20c7c537e6c635930ecb4ace95a54926b3ab77698d9f4922f0b1c58ea87156483b0400040204420404040205000502d80ed601db6501fed602b27201730000d603b27201730100d604e4c672030410d605e4c6a70411d606b27205730200d607b27205730300d608b27205730400d609b27205730500d60a9172097306d60be4c6a7050c63d60cb1720bd60db1a5d60ed9010e0c63b0dc0c0f720e01d9011063db630872107307d90110414d0e9a8c7210018c8c72100202d196830701938cb2db6308720273080001730996830301938cb2db63087203730a0001730b937eb27204730c00057206937eb27204730d0005720792db6903db6503fe720895720ad804d60fe4c6a7050c63d610b2a5b1720f00d611e4c672100411d612b27205730e009683090192c17210c1a793db63087210db6308a793b27211730f00720693b27211731000720793b27211731100997209731293b272117313009a7208721293b27211731400721293e4c67210050c63720f93c27210c2a7efaea5d9010f63aedb6308720fd901114d0e938c7211018cb2db6308a773150001afdc0c1d720b01b4a5731699720c7317d9010f3c636393c48c720f01c48c720f0293720d9a9a720c95720a731873199593cbc2b2a599720d731a00b4e4b2dc640be4c6720204640283010e731be4e3000e731c00731d731e731f732093da720e01a49ada720e01a595720a73217322e3893c0100b44a84993674c57c4fc23c6c1bb221470463e4e711b2260ffd8ed01f1aab420102110504020090ea9db2f261000c6301000008cd02e4cb952261186ec0fd2dc4c2baa8dbfd9c8f6012c5efa9f702f9450a58fe221ee3893c01012aec95af24812a01775de090411ba70a648fe859013f896ca2a1a95882ce5fa08d06000909d9bf168f897d64f00458fc2294adcf89ac0b6e5718cf1199edaa0afc2b2700813096f27f9aedcedda6e766f429c87ecfed43e168c025c4bb2723bb89ff73b400";
+        let constant_bytes = base16::decode(constant_bytes_str).unwrap();
+        let c_res = Constant::sigma_parse_bytes(&constant_bytes);
+        dbg!(&c_res);
+        assert!(c_res.is_err());
+        assert!(matches!(c_res, Err(SigmaParsingError::ValueOutOfBounds(_))));
+    }
 }
diff --git a/ergotree-ir/src/serialization/data.rs b/ergotree-ir/src/serialization/data.rs
index a68f6e1fd..c63ec1bcc 100644
--- a/ergotree-ir/src/serialization/data.rs
+++ b/ergotree-ir/src/serialization/data.rs
@@ -75,7 +75,9 @@ impl DataSerializer {
             // unsupported, see
             // https://github.com/ScorexFoundation/sigmastate-interpreter/issues/659
             Literal::Opt(_) => {
-                return Err(SigmaSerializationError::NotSupported("Option"));
+                return Err(SigmaSerializationError::NotSupported(
+                    "Option serialization is not supported".to_string(),
+                ));
             }
         })
     }
diff --git a/ergotree-ir/src/serialization/serializable.rs b/ergotree-ir/src/serialization/serializable.rs
index 7fe1bf790..b5c6ddea9 100644
--- a/ergotree-ir/src/serialization/serializable.rs
+++ b/ergotree-ir/src/serialization/serializable.rs
@@ -32,7 +32,7 @@ pub enum SigmaSerializationError {
     UnexpectedValue(#[from] TryExtractFromError),
     /// Serialization not supported
     #[error("serialization not supported: {0}")]
-    NotSupported(&'static str),
+    NotSupported(String),
     /// Scorex serialization error
     #[error("Scorex serialization error: {0}")]
     ScorexSerializationError(#[from] ScorexSerializationError),
diff --git a/ergotree-ir/src/serialization/types.rs b/ergotree-ir/src/serialization/types.rs
index 189eeb94f..32a008191 100644
--- a/ergotree-ir/src/serialization/types.rs
+++ b/ergotree-ir/src/serialization/types.rs
@@ -342,7 +342,9 @@ impl SigmaSerializable for SType {
         // for reference see http://github.com/ScorexFoundation/sigmastate-interpreter/blob/25251c1313b0131835f92099f02cef8a5d932b5e/sigmastate/src/main/scala/sigmastate/serialization/TypeSerializer.scala#L25-L25
         use SType::*;
         match self {
-            SType::SFunc(_) => Err(SigmaSerializationError::NotSupported("SFunc")),
+            SType::SFunc(_) => Err(SigmaSerializationError::NotSupported(
+                "SFunc serialization is no supported".to_string(),
+            )),
             SType::SAny => TypeCode::SANY.sigma_serialize(w),
             SType::SUnit => TypeCode::SUNIT.sigma_serialize(w),
             SType::SBoolean => TypeCode::SBOOLEAN.sigma_serialize(w),


Test patch: