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

Issue:
Tx inputs and outputs should be of `BoundedVec<T, 1, u16::MAX>` type
Data inputs can be empty, but max length is also at `u16::MAX` and should be checked.
See node's tx validation:
https://github.com/ergoplatform/ergo/blob/b88f5b759be2e79055a6be1a10b1e6794bff729c/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala#L93-L97



Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/transaction.rs b/bindings/ergo-lib-wasm/src/transaction.rs
index 84ec67739..e4c462414 100644
--- a/bindings/ergo-lib-wasm/src/transaction.rs
+++ b/bindings/ergo-lib-wasm/src/transaction.rs
@@ -83,17 +83,22 @@ impl Transaction {
 
     /// Inputs for transaction
     pub fn inputs(&self) -> Inputs {
-        self.0.inputs.clone().into()
+        self.0.inputs.as_vec().clone().into()
     }
 
     /// Data inputs for transaction
     pub fn data_inputs(&self) -> DataInputs {
-        self.0.data_inputs.clone().into()
+        self.0
+            .data_inputs
+            .clone()
+            .map(|di| di.as_vec().clone())
+            .unwrap_or_default()
+            .into()
     }
 
     /// Output candidates for transaction
     pub fn output_candidates(&self) -> ErgoBoxCandidates {
-        self.0.output_candidates.clone().into()
+        self.0.output_candidates.as_vec().clone().into()
     }
 
     /// Returns ErgoBox's created from ErgoBoxCandidate's with tx id and indices
@@ -122,17 +127,22 @@ impl UnsignedTransaction {
 
     /// Inputs for transaction
     pub fn inputs(&self) -> UnsignedInputs {
-        self.0.inputs.clone().into()
+        self.0.inputs.as_vec().clone().into()
     }
 
     /// Data inputs for transaction
     pub fn data_inputs(&self) -> DataInputs {
-        self.0.data_inputs.clone().into()
+        self.0
+            .clone()
+            .data_inputs
+            .map(|di| di.as_vec().clone())
+            .unwrap_or_default()
+            .into()
     }
 
     /// Output candidates for transaction
     pub fn output_candidates(&self) -> ErgoBoxCandidates {
-        self.0.output_candidates.clone().into()
+        self.0.output_candidates.as_vec().clone().into()
     }
 
     /// JSON representation
diff --git a/ergo-lib/Cargo.toml b/ergo-lib/Cargo.toml
index c9e57843a..56345f7bd 100644
--- a/ergo-lib/Cargo.toml
+++ b/ergo-lib/Cargo.toml
@@ -25,6 +25,8 @@ serde = { version = "1.0", features = ["derive"], optional = true }
 serde_json = "1.0"
 thiserror = "1"
 derive_more = "0.99"
+# bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="f684d78" }
+bounded-vec = { version = "^0.4.0" } 
 
 [features]
 default = ["json", "compiler"]
diff --git a/ergo-lib/src/chain/json/transaction.rs b/ergo-lib/src/chain/json/transaction.rs
index e050cf44f..f270937cc 100644
--- a/ergo-lib/src/chain/json/transaction.rs
+++ b/ergo-lib/src/chain/json/transaction.rs
@@ -44,6 +44,8 @@ mod tests {
 
     proptest! {
 
+        #![proptest_config(ProptestConfig::with_cases(64))]
+
         #[test]
         fn tx_roundtrip(t in any::<Transaction>()) {
             let j = serde_json::to_string(&t)?;
diff --git a/ergo-lib/src/chain/transaction.rs b/ergo-lib/src/chain/transaction.rs
index 2bd5bcc6f..734fd4dd9 100644
--- a/ergo-lib/src/chain/transaction.rs
+++ b/ergo-lib/src/chain/transaction.rs
@@ -4,6 +4,7 @@ mod data_input;
 pub mod input;
 pub mod unsigned;
 
+use bounded_vec::BoundedVec;
 pub use data_input::*;
 use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
 use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
@@ -28,6 +29,7 @@ use proptest_derive::Arbitrary;
 use serde::{Deserialize, Serialize};
 
 use std::convert::TryFrom;
+use std::convert::TryInto;
 use std::iter::FromIterator;
 #[cfg(feature = "json")]
 use thiserror::Error;
@@ -62,6 +64,9 @@ impl From<TxId> for String {
     }
 }
 
+/// BouncedVec type for Tx inputs and output_candidates
+pub type TxIoVec<T> = BoundedVec<T, 1, { u16::MAX as usize }>;
+
 /**
  * ErgoTransaction is an atomic state transition operation. It destroys Boxes from the state
  * and creates new ones. If transaction is spending boxes protected by some non-trivial scripts,
@@ -84,15 +89,15 @@ impl From<TxId> for String {
 pub struct Transaction {
     tx_id: TxId,
     /// inputs, that will be spent by this transaction.
-    pub inputs: Vec<Input>,
+    pub inputs: TxIoVec<Input>,
     /// inputs, that are not going to be spent by transaction, but will be reachable from inputs
     /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not
     /// included in transaction cost and they do not contain spending proofs.
-    pub data_inputs: Vec<DataInput>,
+    pub data_inputs: Option<TxIoVec<DataInput>>,
 
     /// box candidates to be created by this transaction. Differ from [`Self::outputs`] in that
     /// they do not include transaction id and index
-    pub output_candidates: Vec<ErgoBoxCandidate>,
+    pub output_candidates: TxIoVec<ErgoBoxCandidate>,
 
     /// Boxes to be created by this transaction. Differ from [`Self::output_candidates`] in that
     /// they include transaction id and index
@@ -105,13 +110,10 @@ impl Transaction {
 
     /// Creates new transaction
     pub fn new(
-        inputs: Vec<Input>,
-        data_inputs: Vec<DataInput>,
-        output_candidates: Vec<ErgoBoxCandidate>,
+        inputs: TxIoVec<Input>,
+        data_inputs: Option<TxIoVec<DataInput>>,
+        output_candidates: TxIoVec<ErgoBoxCandidate>,
     ) -> Result<Transaction, SigmaSerializationError> {
-        // TODO: use BoundedVec for inputs and outputs
-        // https://github.com/ergoplatform/sigma-rust/issues/327
-        assert!(output_candidates.len() < u16::MAX as usize);
         let tx_to_sign = Transaction {
             tx_id: TxId::zero(),
             inputs,
@@ -139,7 +141,7 @@ impl Transaction {
 
     /// Serialized tx with empty proofs
     pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
-        let empty_proof_inputs = self.inputs.iter().map(|i| i.input_to_sign()).collect();
+        let empty_proof_inputs = self.inputs.mapped_ref(|i| i.input_to_sign());
         let tx_to_sign = Transaction {
             inputs: empty_proof_inputs,
             ..(*self).clone()
@@ -158,10 +160,12 @@ impl SigmaSerializable for Transaction {
         // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala#L112-L112
         w.put_usize_as_u16_unwrapped(self.inputs.len())?;
         self.inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
-        w.put_usize_as_u16_unwrapped(self.data_inputs.len())?;
-        self.data_inputs
-            .iter()
-            .try_for_each(|i| i.sigma_serialize(w))?;
+        if let Some(data_inputs) = &self.data_inputs {
+            w.put_usize_as_u16_unwrapped(data_inputs.len())?;
+            data_inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
+        } else {
+            w.put_u16(0)?;
+        }
 
         // Serialize distinct ids of tokens in transaction outputs.
         // This optimization is crucial to allow up to MaxTokens (== 255) in a box.
@@ -224,7 +228,11 @@ impl SigmaSerializable for Transaction {
             )?)
         }
 
-        Ok(Transaction::new(inputs, data_inputs, outputs)?)
+        Ok(Transaction::new(
+            inputs.try_into()?,
+            data_inputs.try_into().ok(),
+            outputs.try_into()?,
+        )?)
     }
 }
 
@@ -233,8 +241,11 @@ impl From<Transaction> for json::transaction::TransactionJson {
     fn from(v: Transaction) -> Self {
         json::transaction::TransactionJson {
             tx_id: v.tx_id.clone(),
-            inputs: v.inputs.clone(),
-            data_inputs: v.data_inputs.clone(),
+            inputs: v.inputs.as_vec().clone(),
+            data_inputs: v
+                .data_inputs
+                .map(|di| di.as_vec().clone())
+                .unwrap_or_default(),
             outputs: v.outputs,
         }
     }
@@ -250,15 +261,34 @@ pub enum TransactionFromJsonError {
     /// Serialization failed (id calculation)
     #[error("Serialization failed (id calculation)")]
     SerializationError,
+    /// Invalid tx input count
+    #[error("Invalid Tx inputs: {0:?}")]
+    InvalidInputsCount(bounded_vec::BoundedVecOutOfBounds),
+    /// Invalid tx output_candidates count
+    #[error("Invalid Tx output_candidates: {0:?}")]
+    InvalidOutputCandidatesCount(bounded_vec::BoundedVecOutOfBounds),
+    /// Invalid tx data inputs count
+    #[error("Invalid Tx data inputs: {0:?}")]
+    InvalidDataInputsCount(bounded_vec::BoundedVecOutOfBounds),
 }
 
 #[cfg(feature = "json")]
 impl TryFrom<json::transaction::TransactionJson> for Transaction {
     type Error = TransactionFromJsonError;
     fn try_from(tx_json: json::transaction::TransactionJson) -> Result<Self, Self::Error> {
-        let output_candidates = tx_json.outputs.iter().map(|o| o.clone().into()).collect();
-        let tx = Transaction::new(tx_json.inputs, tx_json.data_inputs, output_candidates)
-            .map_err(|_| TransactionFromJsonError::SerializationError)?;
+        let output_candidates: Vec<ErgoBoxCandidate> =
+            tx_json.outputs.iter().map(|o| o.clone().into()).collect();
+        let tx = Transaction::new(
+            tx_json
+                .inputs
+                .try_into()
+                .map_err(TransactionFromJsonError::InvalidInputsCount)?,
+            tx_json.data_inputs.try_into().ok(),
+            output_candidates
+                .try_into()
+                .map_err(TransactionFromJsonError::InvalidOutputCandidatesCount)?,
+        )
+        .map_err(|_| TransactionFromJsonError::SerializationError)?;
         if tx.tx_id == tx_json.tx_id {
             Ok(tx)
         } else {
@@ -270,6 +300,8 @@ impl TryFrom<json::transaction::TransactionJson> for Transaction {
 #[cfg(test)]
 pub mod tests {
 
+    use std::convert::TryInto;
+
     use super::*;
 
     use ergotree_ir::serialization::sigma_serialize_roundtrip;
@@ -286,7 +318,12 @@ pub mod tests {
                 vec(any::<ErgoBoxCandidate>(), 1..10),
             )
                 .prop_map(|(inputs, data_inputs, outputs)| {
-                    Self::new(inputs, data_inputs, outputs).unwrap()
+                    Self::new(
+                        inputs.try_into().unwrap(),
+                        data_inputs.try_into().ok(),
+                        outputs.try_into().unwrap(),
+                    )
+                    .unwrap()
                 })
                 .boxed()
         }
@@ -295,6 +332,8 @@ pub mod tests {
 
     proptest! {
 
+        #![proptest_config(ProptestConfig::with_cases(64))]
+
         #[test]
         fn tx_ser_roundtrip(v in any::<Transaction>()) {
             prop_assert_eq![sigma_serialize_roundtrip(&v), v];
diff --git a/ergo-lib/src/chain/transaction/unsigned.rs b/ergo-lib/src/chain/transaction/unsigned.rs
index ad1d5442a..eef8d9bf2 100644
--- a/ergo-lib/src/chain/transaction/unsigned.rs
+++ b/ergo-lib/src/chain/transaction/unsigned.rs
@@ -5,6 +5,7 @@ use super::input::{Input, UnsignedInput};
 use super::json;
 use super::prover_result::ProverResult;
 use super::DataInput;
+use super::TxIoVec;
 use super::{
     super::{digest32::blake2b256_hash, ergo_box::ErgoBoxCandidate},
     Transaction, TxId,
@@ -15,6 +16,8 @@ use ergotree_ir::serialization::SigmaSerializationError;
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "json")]
 use std::convert::TryFrom;
+#[cfg(feature = "json")]
+use std::convert::TryInto;
 
 /// Unsigned (inputs without proofs) transaction
 #[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
@@ -29,21 +32,21 @@ use std::convert::TryFrom;
 pub struct UnsignedTransaction {
     tx_id: TxId,
     /// unsigned inputs, that will be spent by this transaction.
-    pub inputs: Vec<UnsignedInput>,
+    pub inputs: TxIoVec<UnsignedInput>,
     /// inputs, that are not going to be spent by transaction, but will be reachable from inputs
     /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not
     /// included in transaction cost and they do not contain spending proofs.
-    pub data_inputs: Vec<DataInput>,
+    pub data_inputs: Option<TxIoVec<DataInput>>,
     /// box candidates to be created by this transaction
-    pub output_candidates: Vec<ErgoBoxCandidate>,
+    pub output_candidates: TxIoVec<ErgoBoxCandidate>,
 }
 
 impl UnsignedTransaction {
     /// Creates new transaction
     pub fn new(
-        inputs: Vec<UnsignedInput>,
-        data_inputs: Vec<DataInput>,
-        output_candidates: Vec<ErgoBoxCandidate>,
+        inputs: TxIoVec<UnsignedInput>,
+        data_inputs: Option<TxIoVec<DataInput>>,
+        output_candidates: TxIoVec<ErgoBoxCandidate>,
     ) -> Result<UnsignedTransaction, SigmaSerializationError> {
         let tx_to_sign = UnsignedTransaction {
             tx_id: TxId::zero(),
@@ -70,19 +73,15 @@ 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 empty_proofs_input = self
-            .inputs
-            .iter()
-            .map(|ui| {
-                Input::new(
-                    ui.box_id.clone(),
-                    ProverResult {
-                        proof: ProofBytes::Empty,
-                        extension: ui.extension.clone(),
-                    },
-                )
-            })
-            .collect();
+        let empty_proofs_input = self.inputs.mapped_ref(|ui| {
+            Input::new(
+                ui.box_id.clone(),
+                ProverResult {
+                    proof: ProofBytes::Empty,
+                    extension: ui.extension.clone(),
+                },
+            )
+        });
         let tx = Transaction::new(
             empty_proofs_input,
             self.data_inputs.clone(),
@@ -96,9 +95,12 @@ impl UnsignedTransaction {
 impl From<UnsignedTransaction> for json::transaction::UnsignedTransactionJson {
     fn from(v: UnsignedTransaction) -> Self {
         json::transaction::UnsignedTransactionJson {
-            inputs: v.inputs,
-            data_inputs: v.data_inputs,
-            outputs: v.output_candidates,
+            inputs: v.inputs.as_vec().clone(),
+            data_inputs: v
+                .data_inputs
+                .map(|di| di.as_vec().clone())
+                .unwrap_or_default(),
+            outputs: v.output_candidates.as_vec().clone(),
         }
     }
 }
@@ -108,8 +110,18 @@ impl TryFrom<json::transaction::UnsignedTransactionJson> for UnsignedTransaction
     // We never return this type but () fails to compile (can't format) and ! is experimental
     type Error = String;
     fn try_from(tx_json: json::transaction::UnsignedTransactionJson) -> Result<Self, Self::Error> {
-        UnsignedTransaction::new(tx_json.inputs, tx_json.data_inputs, tx_json.outputs)
-            .map_err(|e| format!("unsigned tx serialization failed: {0}", e))
+        UnsignedTransaction::new(
+            tx_json
+                .inputs
+                .try_into()
+                .map_err(|e: bounded_vec::BoundedVecOutOfBounds| e.to_string())?,
+            tx_json.data_inputs.try_into().ok(),
+            tx_json
+                .outputs
+                .try_into()
+                .map_err(|e: bounded_vec::BoundedVecOutOfBounds| e.to_string())?,
+        )
+        .map_err(|e| format!("unsigned tx serialization failed: {0}", e))
     }
 }
 
@@ -130,7 +142,12 @@ pub mod tests {
                 vec(any::<ErgoBoxCandidate>(), 1..10),
             )
                 .prop_map(|(inputs, data_inputs, outputs)| {
-                    Self::new(inputs, data_inputs, outputs).unwrap()
+                    Self::new(
+                        inputs.try_into().unwrap(),
+                        data_inputs.try_into().ok(),
+                        outputs.try_into().unwrap(),
+                    )
+                    .unwrap()
                 })
                 .boxed()
         }
diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs
index 33ea41b2d..e7d47a7f9 100644
--- a/ergo-lib/src/wallet/signing.rs
+++ b/ergo-lib/src/wallet/signing.rs
@@ -142,8 +142,7 @@ pub fn sign_transaction(
 ) -> Result<Transaction, TxSigningError> {
     let tx = tx_context.spending_tx.clone();
     let message_to_sign = tx.bytes_to_sign()?;
-    let mut signed_inputs: Vec<Input> = vec![];
-    tx.inputs.iter().enumerate().try_for_each(|(idx, input)| {
+    let signed_inputs = tx.inputs.enumerated().try_mapped(|(idx, input)| {
         if let Some(input_box) = tx_context
             .boxes_to_spend
             .iter()
@@ -158,10 +157,7 @@ pub fn sign_transaction(
                     message_to_sign.as_slice(),
                     &HintsBag::empty(),
                 )
-                .map(|proof| {
-                    let input = Input::new(input.box_id.clone(), proof.into());
-                    signed_inputs.push(input);
-                })
+                .map(|proof| Input::new(input.box_id.clone(), proof.into()))
                 .map_err(|e| TxSigningError::ProverError(e, idx))
         } else {
             Err(TxSigningError::InputBoxNotFound(idx))
@@ -198,6 +194,7 @@ mod tests {
     use ergotree_ir::ergo_tree::ErgoTree;
     use ergotree_ir::mir::expr::Expr;
     use std::convert::TryFrom;
+    use std::convert::TryInto;
     use std::rc::Rc;
 
     fn verify_tx_proofs(
@@ -250,7 +247,8 @@ mod tests {
             let candidate = ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, ergo_tree, 0)
                 .build().unwrap();
             let output_candidates = vec![candidate];
-            let tx = UnsignedTransaction::new(inputs, vec![], output_candidates).unwrap();
+            let tx = UnsignedTransaction::new(inputs.try_into().unwrap(),
+                None, output_candidates.try_into().unwrap()).unwrap();
             let tx_context = TransactionContext { spending_tx: tx,
                                                   boxes_to_spend: boxes_to_spend.clone(), data_boxes: vec![] };
             let res = sign_transaction(Box::new(prover).as_ref(), tx_context, &ErgoStateContext::dummy());
diff --git a/ergo-lib/src/wallet/tx_builder.rs b/ergo-lib/src/wallet/tx_builder.rs
index 58d138e1c..23b765246 100644
--- a/ergo-lib/src/wallet/tx_builder.rs
+++ b/ergo-lib/src/wallet/tx_builder.rs
@@ -1,7 +1,9 @@
 //! Builder for an UnsignedTransaction
 
 use std::collections::HashSet;
+use std::convert::TryInto;
 
+use bounded_vec::BoundedVecOutOfBounds;
 use ergotree_interpreter::sigma_protocol;
 use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
 use ergotree_ir::address::{Address, AddressEncoder, NetworkPrefix};
@@ -107,22 +109,18 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
     /// Estimated serialized transaction size in bytes after signing (assuming P2PK box spending)
     pub fn estimate_tx_size_bytes(&self) -> Result<usize, TxBuilderError> {
         let tx = self.build_tx()?;
-        let inputs = tx
-            .inputs
-            .iter()
-            .map(|ui| {
-                // mock proof of the size of ProveDlog's proof (P2PK box spending)
-                // as it's the most often used proof
-                let proof = ProofBytes::Some(vec![0u8, sigma_protocol::SOUNDNESS_BYTES as u8]);
-                Input::new(
-                    ui.box_id.clone(),
-                    crate::chain::transaction::input::prover_result::ProverResult {
-                        proof,
-                        extension: ui.extension.clone(),
-                    },
-                )
-            })
-            .collect();
+        let inputs = tx.inputs.mapped(|ui| {
+            // mock proof of the size of ProveDlog's proof (P2PK box spending)
+            // as it's the most often used proof
+            let proof = ProofBytes::Some(vec![0u8, sigma_protocol::SOUNDNESS_BYTES as u8]);
+            Input::new(
+                ui.box_id.clone(),
+                crate::chain::transaction::input::prover_result::ProverResult {
+                    proof,
+                    extension: ui.extension,
+                },
+            )
+        });
         let signed_tx_mock = Transaction::new(inputs, tx.data_inputs, tx.output_candidates)?;
         Ok(signed_tx_mock.sigma_serialize_bytes()?.len())
     }
@@ -226,9 +224,10 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
                 .clone()
                 .into_iter()
                 .map(UnsignedInput::from)
-                .collect(),
-            self.data_inputs.clone(),
-            output_candidates,
+                .collect::<Vec<UnsignedInput>>()
+                .try_into()?,
+            self.data_inputs.clone().try_into().ok(),
+            output_candidates.try_into()?,
         )?)
     }
 
@@ -278,6 +277,9 @@ pub enum TxBuilderError {
     /// Tx serialization failed (id calculation)
     #[error("Transaction serialization failed: {0}")]
     SerializationError(#[from] SigmaSerializationError),
+    /// Invalid Tx input count
+    #[error("Invalid tx inputs count: {0}")]
+    InvalidInputsCount(#[from] BoundedVecOutOfBounds),
 }
 
 #[cfg(test)]
@@ -594,7 +596,7 @@ mod tests {
             prop_assert!(tx.output_candidates.iter().any(|b| {
                 b.value == miners_fee
             }), "box with miner's fee {:?} is not found in outputs: {:?}", miners_fee, tx.output_candidates);
-            prop_assert_eq!(tx.data_inputs, data_inputs, "unexpected data inputs");
+            prop_assert_eq!(tx.data_inputs.map(|i| i.as_vec().clone()).unwrap_or_default(), data_inputs, "unexpected data inputs");
         }
     }
 }
diff --git a/ergotree-ir/Cargo.toml b/ergotree-ir/Cargo.toml
index 6eb0dbd89..b2ab1d054 100644
--- a/ergotree-ir/Cargo.toml
+++ b/ergotree-ir/Cargo.toml
@@ -28,8 +28,8 @@ proptest-derive = {version = "0.3.0", optional = true }
 bs58 = "0.4.0"
 base16 = "0.2.1"
 num-bigint = "0.4.0"
-# bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="78f1c83" }
-bounded-vec = { version = "^0.3.0" }
+#bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="f684d78" }
+bounded-vec = { version = "^0.4.0" } 
 num256 = "0.3.1"
 num-traits = "0.2.14"
 num-derive = "0.3.3"
diff --git a/ergotree-ir/src/serialization/serializable.rs b/ergotree-ir/src/serialization/serializable.rs
index a4c1cda79..51e70a306 100644
--- a/ergotree-ir/src/serialization/serializable.rs
+++ b/ergotree-ir/src/serialization/serializable.rs
@@ -95,6 +95,9 @@ pub enum SigmaParsingError {
     /// Serialization error
     #[error("serialization error: {0}")]
     SerializationError(#[from] SigmaSerializationError),
+    /// Invalid item quantity in BoundedVec
+    #[error("Invalid item quantity in BoundedVec: {0}")]
+    BoundedVecOutOfBounds(#[from] BoundedVecOutOfBounds),
 }
 
 impl From<io::Error> for SigmaParsingError {
@@ -109,12 +112,6 @@ impl From<&io::Error> for SigmaParsingError {
     }
 }
 
-impl From<BoundedVecOutOfBounds> for SigmaParsingError {
-    fn from(e: BoundedVecOutOfBounds) -> Self {
-        SigmaParsingError::ValueOutOfBounds(format!("{:?}", e))
-    }
-}
-
 impl From<TypeUnificationError> for SigmaParsingError {
     fn from(e: TypeUnificationError) -> Self {
         SigmaParsingError::Misc(format!("{:?}", e))
diff --git a/ergotree-ir/src/types/stype_param.rs b/ergotree-ir/src/types/stype_param.rs
index 28982eb89..b370b3ed6 100644
--- a/ergotree-ir/src/types/stype_param.rs
+++ b/ergotree-ir/src/types/stype_param.rs
@@ -14,19 +14,12 @@ use crate::serialization::SigmaSerializeResult;
 use super::stype::SType;
 
 /// Type variable for generic signatures
-#[derive(PartialEq, Eq, Clone)]
+#[derive(PartialEq, Eq, Clone, Hash)]
 pub struct STypeVar {
     /// Type variable name (e.g. "T")
     name_bytes: BoundedVec<u8, 1, 254>,
 }
 
-#[allow(clippy::derive_hash_xor_eq)]
-impl Hash for STypeVar {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.name_bytes.as_vec().hash(state);
-    }
-}
-
 impl std::fmt::Debug for STypeVar {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         self.as_string().fmt(f)


Test patch:
diff --git a/ergo-lib/proptest-regressions/chain/address.txt b/ergo-lib/proptest-regressions/chain/address.txt
deleted file mode 100644
index 9134112da..000000000
--- a/ergo-lib/proptest-regressions/chain/address.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Seeds for failure cases proptest has generated in the past. It is
-# automatically read and these particular cases re-run before any
-# novel cases are generated.
-#
-# It is recommended to check this file in to source control so that
-# everyone who runs the test benefits from these saved cases.
-cc f93d83e598eed1f0169b46accd6aa39995ec9e103410359e84275568ab997a78 # shrinks to s = "a"
diff --git a/ergo-lib/proptest-regressions/chain/ergo_box.txt b/ergo-lib/proptest-regressions/chain/ergo_box.txt
deleted file mode 100644
index 9e215b4f6..000000000
--- a/ergo-lib/proptest-regressions/chain/ergo_box.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Seeds for failure cases proptest has generated in the past. It is
-# automatically read and these particular cases re-run before any
-# novel cases are generated.
-#
-# It is recommended to check this file in to source control so that
-# everyone who runs the test benefits from these saved cases.
-cc b597aa9726ae06f03acce6d22d35c0ebe1f6574f2e88eb2a2db75d114a02ff65 # shrinks to v = ErgoBoxCandidate { value: 0, ergo_tree: ErgoTree { header: ErgoTreeHeader(0), constants: [], root: Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProveDlog(EcPointType))) }) }, tokens: [], additional_registers: {}, creation_height: 0 }
diff --git a/ergo-lib/proptest-regressions/chain/transaction.txt b/ergo-lib/proptest-regressions/chain/transaction.txt
deleted file mode 100644
index bec2c69db..000000000
--- a/ergo-lib/proptest-regressions/chain/transaction.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Seeds for failure cases proptest has generated in the past. It is
-# automatically read and these particular cases re-run before any
-# novel cases are generated.
-#
-# It is recommended to check this file in to source control so that
-# everyone who runs the test benefits from these saved cases.
-cc e9c483c8c8b81aa908794575e1de2bbb50603e88a71ef3941937379adb3b9a01 # shrinks to v = Transaction { inputs: [Input { box_id: BoxId([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), spending_proof: ProverResult { proof: [], extension: ContextExtension { values: {} } } }], data_inputs: [], outputs: [ErgoBoxCandidate { value: 0, ergo_tree: ErgoTree { header: ErgoTreeHeader(0), constants: [], root: Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProveDlog(EcPointType))) }) }, tokens: [], additional_registers: {}, creation_height: 0 }] }
diff --git a/ergo-lib/proptest-regressions/wallet/tx_builder.txt b/ergo-lib/proptest-regressions/wallet/tx_builder.txt
index 34ec5ed1f..6ef584b7f 100644
--- a/ergo-lib/proptest-regressions/wallet/tx_builder.txt
+++ b/ergo-lib/proptest-regressions/wallet/tx_builder.txt
@@ -6,3 +6,4 @@
 # everyone who runs the test benefits from these saved cases.
 cc 8bd0f5c261309fb2ee37b30b74e9991d23b8faebeb1cdc4c093267418b8f2def # shrinks to inputs = [ErgoBox { box_id: BoxId(Digest32([192, 169, 0, 117, 37, 2, 92, 55, 182, 174, 94, 69, 47, 33, 232, 18, 103, 232, 166, 28, 148, 73, 133, 10, 52, 224, 177, 244, 135, 12, 11, 3])), value: BoxValue(27740066), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0, transaction_id: TxId(Digest32([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), index: 0 }], outputs = [ErgoBoxCandidate { value: BoxValue(13356530), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0 }], change_address = P2PK(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) }), miners_fee = BoxValue(14368777)
 cc 202e4da03d96c5ce67e6c229a270638be71a51f2ef86dc70c2e121cbdf75b782 # shrinks to inputs = [ErgoBox { box_id: BoxId(Digest32([71, 138, 231, 27, 15, 127, 230, 252, 65, 149, 214, 218, 25, 86, 145, 54, 2, 11, 164, 211, 227, 204, 2, 29, 19, 152, 230, 90, 118, 164, 104, 118])), value: BoxValue(22258517), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0, transaction_id: TxId(Digest32([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), index: 0 }], outputs = [ErgoBoxCandidate { value: BoxValue(20403935), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0 }], change_address = P2PK(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) }), miners_fee = BoxValue(1839823)
+cc 4343e84399d6da07ff8bcf4d964e8f955306d4cf98f105c391ed1588ca4def25 # shrinks to inputs = [ErgoBox { box_id: BoxId("2fcc49cd3f0fb0dc051536575e151e3a4bba3e0aa98c6ed5894335fb4266474b"), value: BoxValue(54000000), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([Constant { tpe: STuple(STuple { items: BoundedVec { inner: [SColl(SByte), SGroupElement, SSigmaProp] } }), v: Tup(BoundedVec { inner: [Coll(NativeColl(CollByte([-37, 21, 126, 114, 50, -69, 70, -99, 111, -117, -27, 71, -67, -113, -80, 3, 108, 0, -20, -101, 93, 99, -119, 96, 32, -50, -82, -59, 94, -58, 100, -5, -2, -110, 124, -124, -54, 23, 123, -17, 67, -18, 117, 116, 94, -40, 9, 113, -39, -63, -33, -18, 21, -109, -64, -23, -66, 68, 109, -8, 37, 63, 55, 80, -56, -18, -18, -84, 14, -87, 50, -34]))), GroupElement(EC:000000000000000000000000000000000000000000000000000000000000000000), SigmaProp(SigmaProp(SigmaConjecture(Cand(Cand { items: BoundedVec { inner: [ProofOfKnowledge(ProveDhTuple(ProveDhTuple { g: EC:000000000000000000000000000000000000000000000000000000000000000000, h: EC:000000000000000000000000000000000000000000000000000000000000000000, u: EC:000000000000000000000000000000000000000000000000000000000000000000, v: EC:000000000000000000000000000000000000000000000000000000000000000000 })), ProofOfKnowledge(ProveDlog(ProveDlog { h: EC:000000000000000000000000000000000000000000000000000000000000000000 })), ProofOfKnowledge(ProveDhTuple(ProveDhTuple { g: EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, h: EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, u: EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, v: EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 }))] } }))))] }) }, Constant { tpe: SColl(SBigInt), v: Coll(WrappedColl { elem_tpe: SBigInt, items: [BigInt(BigInt256(Int256(-3305632704798772324))), BigInt(BigInt256(Int256(-3305632704798772324)))] }) }, Constant { tpe: SColl(SBoolean), v: Coll(WrappedColl { elem_tpe: SBoolean, items: [Boolean(false)] }) }]), creation_height: 0, transaction_id: TxId("3f77567114c29e0da67244e4a22720b11405a696804e960f1dfc8b5759ee5da6"), index: 12804 }], outputs = [ErgoBoxCandidate { value: BoxValue(14049207), ergo_tree: ErgoTree { header: ErgoTreeHeader(9), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EC:000000000000000000000000000000000000000000000000000000000000000000 })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([Constant { tpe: SColl(SSigmaProp), v: Coll(WrappedColl { elem_tpe: SSigmaProp, items: [] }) }, Constant { tpe: SColl(SGroupElement), v: Coll(WrappedColl { elem_tpe: SGroupElement, items: [GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), GroupElement(EC:0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)] }) }, Constant { tpe: SColl(SInt), v: Coll(WrappedColl { elem_tpe: SInt, items: [] }) }, Constant { tpe: SColl(SColl(SByte)), v: Coll(WrappedColl { elem_tpe: SColl(SByte), items: [Coll(NativeColl(CollByte([8, -43, -88, -112, 93, -4, -22, 0, 43, 53, 127, -70, 73, -24, 49, -94, -92, 91, 5, 20, -100, 96, -8, -14, 9, -47, -87, 12, 61, -114, -55, -104, 100, 17, 35, -105, -72, 105, 108, 99, -27, -34, -4, 19, 70, 100, -72, -50, -55, 52, 19, 8, 113, 127, -76, -6, 16, 79, -18, 20, -80, -27, 108, -83, 19, -49, 96, -70, 69, 21, 70, -66, -97, -116, -125, 72, 23, 125, 94, -70, -37])))] }) }, Constant { tpe: SColl(SShort), v: Coll(WrappedColl { elem_tpe: SShort, items: [Short(-30184), Short(-30184)] }) }]), creation_height: 121257243 }], change_address = P2S([16, 2, 8, 150, 2, 206, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 206, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 8, 206, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 235, 2, 115, 0, 115, 1]), miners_fee = BoxValue(2093460), data_inputs = []