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

Issue:
[150 SigmaUSD] Handle `TransactionContext::boxes_to_spend` order != `TransactionContext::spending_tx::inputs` order (same for data inputs)
### `TransactionContext`
Make `TransactionContext::boxes_to_spend` and `data_boxes` private (+ use `TxIoVec` type) exposing them via getters and add checking ctor `new` failing if an id from `UnsignedTransaction::inputs` is not found in `boxes_to_spend` and an id from `UnsignedTransaction::data_inputs` is not found in `data_boxes`. 

### `signing::make_context`
- Find `self_box` by first finding it's id in `TransactionContext::spending_tx::inputs`;
- Make `Context::inputs` by mapping `TransactionContext::spending_tx::inputs` and finding boxes for input ids in `TransactionContext::boxes_to_spend`;
- Make `Context::data_inputs` by mapping `TransactionContext::spending_tx::data_inputs` and finding boxes for data input ids in `TransactionContext::data_boxes`;

### Testing
A good regression test would be to pass `boxes_to_spend` and `data_boxes` in reversed order of `TransactionContext::spending_tx::inputs` and `data_inputs` and check that `Context::self_box`, `inputs` and `data_inputs` are in correctorder (the same as in `TransactionContext::spending_tx`).


Discussion / hints:
Starting this one.
@greenhat You require that `TransactionContext::boxes_to_spend` and `data_boxes` use `TxIoVec`. Should I also make the change for `Context::inputs` and `Context::data_inputs` (~~it'll have downstream effects on `coll_[filter|fold|map]`~~ Only affects prop-tests in those modules)?
> @greenhat You require that `TransactionContext::boxes_to_spend` and `data_boxes` use `TxIoVec`. Should I also make the change for `Context::inputs` and `Context::data_inputs` (~it'll have downstream effects on `coll_[filter|fold|map]`~ Only affects prop-tests in those modules)?

Good question! `Context` is more like the interpreter's internal representation and since in the ErgoScript/ErgoTree type system there is no such thing as bounded vector/collection we don't get any benefits in the evaluation code from using `TxIoVec`. However, using `TxIoVec` in `Context` protects us from using incorrect quantities, and given that `Context` can be constructed not only from `TransactionContext` I think it's a good idea to use `TxIoVec` in here.
Will do, thanks!

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/error_conversion.rs b/bindings/ergo-lib-wasm/src/error_conversion.rs
index 21b736ceb..8ec8fcc7d 100644
--- a/bindings/ergo-lib-wasm/src/error_conversion.rs
+++ b/bindings/ergo-lib-wasm/src/error_conversion.rs
@@ -4,6 +4,7 @@ use std::array::TryFromSliceError;
 use std::num::ParseIntError;
 
 use base16::DecodeError;
+use bounded_vec::BoundedVecOutOfBounds;
 use ergo_lib::ergotree_ir::chain::address::AddressEncoderError;
 use ergo_lib::ergotree_ir::chain::address::AddressError;
 use ergo_lib::ergotree_ir::chain::digest32::Digest32Error;
@@ -75,3 +76,4 @@ from_error_to_wrap_via_debug!(ErgoTreeConstantError);
 from_error_to_wrap_via_debug!(ErgoTreeConstantsParsingError);
 from_error_to_wrap_via_debug!(ParseIntError);
 from_error_to_wrap_via_debug!(ChildIndexError);
+from_error_to_wrap_via_debug!(BoundedVecOutOfBounds);
diff --git a/bindings/ergo-lib-wasm/src/transaction/reduced.rs b/bindings/ergo-lib-wasm/src/transaction/reduced.rs
index c7ce81be4..2a7452d05 100644
--- a/bindings/ergo-lib-wasm/src/transaction/reduced.rs
+++ b/bindings/ergo-lib-wasm/src/transaction/reduced.rs
@@ -7,6 +7,7 @@ use crate::ergo_state_ctx::ErgoStateContext;
 use crate::error_conversion::to_js;
 
 use ergo_lib::chain::transaction::reduced::reduce_tx;
+use ergo_lib::chain::transaction::TxIoVec;
 use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
 use wasm_bindgen::prelude::*;
 
@@ -31,15 +32,21 @@ impl ReducedTransaction {
         data_boxes: &ErgoBoxes,
         state_context: &ErgoStateContext,
     ) -> Result<ReducedTransaction, JsValue> {
-        let boxes_to_spend: Vec<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox> =
-            boxes_to_spend.clone().into();
-        let data_boxes: Vec<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox> =
-            data_boxes.clone().into();
-        let tx_context = ergo_lib::wallet::signing::TransactionContext {
-            spending_tx: unsigned_tx.clone().into(),
+        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
+        let data_boxes = {
+            let d: Vec<_> = data_boxes.clone().into();
+            if d.is_empty() {
+                None
+            } else {
+                Some(TxIoVec::from_vec(d).map_err(to_js)?)
+            }
+        };
+        let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+            unsigned_tx.clone().into(),
             boxes_to_spend,
             data_boxes,
-        };
+        )
+        .map_err(to_js)?;
         reduce_tx(tx_context, &state_context.clone().into())
             .map_err(to_js)
             .map(ReducedTransaction::from)
diff --git a/bindings/ergo-lib-wasm/src/wallet.rs b/bindings/ergo-lib-wasm/src/wallet.rs
index 492c4fd8a..dca5b6619 100644
--- a/bindings/ergo-lib-wasm/src/wallet.rs
+++ b/bindings/ergo-lib-wasm/src/wallet.rs
@@ -1,5 +1,5 @@
 //! Wallet-like features
-use ergo_lib::ergotree_ir::chain;
+use ergo_lib::chain::transaction::TxIoVec;
 use wasm_bindgen::prelude::*;
 
 pub mod derivation_path;
@@ -41,13 +41,21 @@ impl Wallet {
         boxes_to_spend: &ErgoBoxes,
         data_boxes: &ErgoBoxes,
     ) -> Result<Transaction, JsValue> {
-        let boxes_to_spend: Vec<chain::ergo_box::ErgoBox> = boxes_to_spend.clone().into();
-        let data_boxes: Vec<chain::ergo_box::ErgoBox> = data_boxes.clone().into();
-        let tx_context = ergo_lib::wallet::signing::TransactionContext {
-            spending_tx: tx.clone().into(),
+        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
+        let data_boxes = {
+            let d: Vec<_> = data_boxes.clone().into();
+            if d.is_empty() {
+                None
+            } else {
+                Some(TxIoVec::from_vec(d).map_err(to_js)?)
+            }
+        };
+        let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+            tx.clone().into(),
             boxes_to_spend,
             data_boxes,
-        };
+        )
+        .map_err(to_js)?;
         self.0
             .sign_transaction(tx_context, &_state_context.clone().into())
             .map_err(to_js)
diff --git a/ergo-lib/src/chain/transaction.rs b/ergo-lib/src/chain/transaction.rs
index 77cf3bcac..d1c97f9f4 100644
--- a/ergo-lib/src/chain/transaction.rs
+++ b/ergo-lib/src/chain/transaction.rs
@@ -5,7 +5,7 @@ pub mod input;
 pub mod reduced;
 pub mod unsigned;
 
-use bounded_vec::BoundedVec;
+pub use ergotree_interpreter::eval::context::TxIoVec;
 use ergotree_ir::chain::digest32::blake2b256_hash;
 use ergotree_ir::chain::ergo_box::ErgoBox;
 use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
@@ -35,9 +35,6 @@ use std::convert::TryFrom;
 use std::convert::TryInto;
 use std::iter::FromIterator;
 
-/// 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,
diff --git a/ergo-lib/src/chain/transaction/reduced.rs b/ergo-lib/src/chain/transaction/reduced.rs
index adaf4eb01..30a4c10c4 100644
--- a/ergo-lib/src/chain/transaction/reduced.rs
+++ b/ergo-lib/src/chain/transaction/reduced.rs
@@ -66,12 +66,15 @@ pub fn reduce_tx(
     state_context: &ErgoStateContext,
 ) -> Result<ReducedTransaction, TxSigningError> {
     let tx = &tx_context.spending_tx;
-    let reduced_inputs = tx.inputs.clone().enumerated().try_mapped(|(idx, input)| {
-        if let Some(input_box) = tx_context
-            .boxes_to_spend
-            .iter()
-            .find(|b| b.box_id() == input.box_id)
-        {
+    let reduced_inputs = tx
+        .inputs
+        .clone()
+        .enumerated()
+        .try_mapped::<_, _, TxSigningError>(|(idx, input)| {
+            let input_box = tx_context
+                .get_boxes_to_spend()
+                .find(|b| b.box_id() == input.box_id)
+                .ok_or(TxSigningError::InputBoxNotFound(idx))?;
             let ctx = Rc::new(make_context(state_context, &tx_context, idx)?);
             let expr = input_box
                 .ergo_tree
@@ -85,10 +88,7 @@ pub fn reduce_tx(
                 reduction_result,
                 extension: input.extension,
             })
-        } else {
-            Err(TxSigningError::InputBoxNotFound(idx))
-        }
-    })?;
+        })?;
     Ok(ReducedTransaction {
         unsigned_tx: tx.clone(),
         reduced_inputs,
diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs
index 481b6aadb..8410ee203 100644
--- a/ergo-lib/src/wallet/signing.rs
+++ b/ergo-lib/src/wallet/signing.rs
@@ -12,7 +12,7 @@ use crate::chain::{
     transaction::{unsigned::UnsignedTransaction, Transaction},
 };
 
-use ergotree_interpreter::eval::context::Context;
+use ergotree_interpreter::eval::context::{Context, TxIoVec};
 use ergotree_interpreter::eval::env::Env;
 use ergotree_interpreter::sigma_protocol::prover::ProverError;
 use ergotree_interpreter::sigma_protocol::prover::{ContextExtension, Prover};
@@ -21,12 +21,15 @@ use thiserror::Error;
 /// Errors on transaction signing
 #[derive(Error, PartialEq, Eq, Debug, Clone)]
 pub enum TxSigningError {
-    /// error on proving an input
+    /// Error on proving an input
     #[error("Prover error (tx input index {1}): {0}")]
     ProverError(ProverError, usize),
-    /// failed to find an input in boxes_to_spend
+    /// Failed to find an input in `boxes_to_spend`
     #[error("Input box not found (index {0})")]
     InputBoxNotFound(usize),
+    /// Failed to find a data input in `data_boxes`
+    #[error("Data input box not found (index {0})")]
+    DataInputBoxNotFound(usize),
     /// Context creation error
     #[error("Context error: {0}")]
     ContextError(String),
@@ -41,23 +44,68 @@ pub struct TransactionContext {
     /// Unsigned transaction to sign
     pub spending_tx: UnsignedTransaction,
     /// Boxes corresponding to [`UnsignedTransaction::inputs`]
-    pub boxes_to_spend: Vec<ErgoBox>,
+    boxes_to_spend: TxIoVec<ErgoBox>,
     /// Boxes corresponding to [`UnsignedTransaction::data_inputs`]
-    pub data_boxes: Vec<ErgoBox>,
+    data_boxes: Option<TxIoVec<ErgoBox>>,
 }
 
-/// `self_index` - index of the SELF box in the tx_ctx.boxes_to_spend
+impl TransactionContext {
+    /// Return a `TransactionContext` instance if and only if every unsigned transaction input has
+    /// an associated box within `boxes_to_spend` and every data input has an associated box within
+    /// `data_boxes`.
+    pub fn new(
+        spending_tx: UnsignedTransaction,
+        boxes_to_spend: TxIoVec<ErgoBox>,
+        data_boxes: Option<TxIoVec<ErgoBox>>,
+    ) -> Result<Self, TxSigningError> {
+        for (i, unsigned_input) in spending_tx.inputs.iter().enumerate() {
+            if !boxes_to_spend
+                .iter()
+                .any(|b| unsigned_input.box_id == b.box_id())
+            {
+                return Err(TxSigningError::InputBoxNotFound(i));
+            }
+        }
+        if let Some(data_inputs) = spending_tx.data_inputs.as_ref() {
+            if let Some(data_boxes) = data_boxes.as_ref() {
+                for (i, data_input) in data_inputs.iter().enumerate() {
+                    if !data_boxes.iter().any(|b| data_input.box_id == b.box_id()) {
+                        return Err(TxSigningError::DataInputBoxNotFound(i));
+                    }
+                }
+            } else {
+                return Err(TxSigningError::DataInputBoxNotFound(0));
+            }
+        }
+        Ok(TransactionContext {
+            spending_tx,
+            boxes_to_spend,
+            data_boxes,
+        })
+    }
+
+    /// Get boxes corresponding to [`UnsignedTransaction::inputs`]
+    pub fn get_boxes_to_spend(&self) -> impl Iterator<Item = &ErgoBox> {
+        self.boxes_to_spend.iter()
+    }
+}
+
+/// `self_index` - index of the SELF box in the tx_ctx.spending_tx.inputs
 pub fn make_context(
     state_ctx: &ErgoStateContext,
     tx_ctx: &TransactionContext,
     self_index: usize,
 ) -> Result<Context, TxSigningError> {
     let height = state_ctx.pre_header.height;
+
+    // Find self_box by matching BoxIDs
     let self_box = tx_ctx
         .boxes_to_spend
-        .get(self_index)
+        .iter()
+        .find(|b| b.box_id() == tx_ctx.spending_tx.inputs.as_vec()[self_index].box_id)
         .cloned()
         .ok_or_else(|| TxSigningError::ContextError("self_index is out of bounds".to_string()))?;
+
     let outputs = tx_ctx
         .spending_tx
         .output_candidates
@@ -65,16 +113,36 @@ pub fn make_context(
         .enumerate()
         .map(|(idx, b)| ErgoBox::from_box_candidate(b, tx_ctx.spending_tx.id(), idx as u16))
         .collect::<Result<Vec<ErgoBox>, SigmaSerializationError>>()?;
-    let data_inputs: Vec<ErgoBox> = tx_ctx.data_boxes.clone();
+    let data_inputs_ir = if let Some(data_inputs) = tx_ctx.spending_tx.data_inputs.as_ref() {
+        Some(data_inputs.clone().enumerated().try_mapped(|(idx, di)| {
+            tx_ctx
+                .data_boxes
+                .as_ref()
+                .ok_or(TxSigningError::DataInputBoxNotFound(idx))?
+                .iter()
+                .find(|b| di.box_id == b.box_id())
+                .map(|b| Rc::new(b.clone()))
+                .ok_or(TxSigningError::DataInputBoxNotFound(idx))
+        })?)
+    } else {
+        None
+    };
     let self_box_ir = Rc::new(self_box);
     let outputs_ir = outputs.into_iter().map(Rc::new).collect();
     let inputs_ir = tx_ctx
-        .boxes_to_spend
+        .spending_tx
+        .inputs
         .clone()
-        .into_iter()
-        .map(Rc::new)
-        .collect();
-    let data_inputs_ir = data_inputs.into_iter().map(Rc::new).collect();
+        .enumerated()
+        .try_mapped(|(idx, u)| {
+            tx_ctx
+                .boxes_to_spend
+                .iter()
+                .find(|b| u.box_id == b.box_id())
+                .map(|b| Rc::new(b.clone()))
+                .ok_or(TxSigningError::InputBoxNotFound(idx))
+        })?;
+
     Ok(Context {
         height,
         self_box: self_box_ir,
@@ -96,25 +164,22 @@ pub fn sign_transaction(
     let tx = tx_context.spending_tx.clone();
     let message_to_sign = tx.bytes_to_sign()?;
     let signed_inputs = tx.inputs.enumerated().try_mapped(|(idx, input)| {
-        if let Some(input_box) = tx_context
+        let input_box = tx_context
             .boxes_to_spend
             .iter()
             .find(|b| b.box_id() == input.box_id)
-        {
-            let ctx = Rc::new(make_context(state_context, &tx_context, idx)?);
-            prover
-                .prove(
-                    &input_box.ergo_tree,
-                    &Env::empty(),
-                    ctx,
-                    message_to_sign.as_slice(),
-                    &HintsBag::empty(),
-                )
-                .map(|proof| Input::new(input.box_id.clone(), proof.into()))
-                .map_err(|e| TxSigningError::ProverError(e, idx))
-        } else {
-            Err(TxSigningError::InputBoxNotFound(idx))
-        }
+            .ok_or(TxSigningError::InputBoxNotFound(idx))?;
+        let ctx = Rc::new(make_context(state_context, &tx_context, idx)?);
+        prover
+            .prove(
+                &input_box.ergo_tree,
+                &Env::empty(),
+                ctx,
+                message_to_sign.as_slice(),
+                &HintsBag::empty(),
+            )
+            .map(|proof| Input::new(input.box_id.clone(), proof.into()))
+            .map_err(|e| TxSigningError::ProverError(e, idx))
     })?;
     Ok(Transaction::new(
         signed_inputs,
@@ -174,9 +239,11 @@ mod tests {
     use sigma_test_util::force_any_val;
 
     use crate::chain::transaction::reduced::reduce_tx;
+    use crate::chain::transaction::DataInput;
     use crate::chain::{
         ergo_box::box_builder::ErgoBoxCandidateBuilder, transaction::UnsignedInput,
     };
+    use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
     use ergotree_ir::ergo_tree::ErgoTree;
     use ergotree_ir::mir::expr::Expr;
     use std::convert::TryFrom;
@@ -236,7 +303,7 @@ mod tests {
             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![] };
+                                                  boxes_to_spend: TxIoVec::from_vec(boxes_to_spend.clone()).unwrap(), data_boxes: None };
             let res = sign_transaction(prover.as_ref(), tx_context.clone(), &force_any_val::<ErgoStateContext>());
             let signed_tx = res.unwrap();
             prop_assert!(verify_tx_proofs(&signed_tx, &boxes_to_spend).unwrap());
@@ -246,6 +313,67 @@ mod tests {
         }
     }
 
+    proptest! {
+        #![proptest_config(ProptestConfig::with_cases(16))]
+
+        #[test]
+        fn test_tx_context_input_reorderings(
+            inputs in vec((any::<ErgoBox>(), any::<ContextExtension>()), 1..10),
+            mut data_input_boxes in vec(any::<ErgoBox>(), 1..10),
+            candidate in any::<ErgoBoxCandidate>(),
+        ) {
+          let num_inputs = inputs.len();
+          let ut_inputs: Vec<_> = inputs
+              .iter()
+              .map(|(b, extension)|
+                  UnsignedInput {
+                    box_id: b.box_id(),
+                    extension: extension.clone(),
+                  }
+              )
+              .collect();
+          let ut_inputs = TxIoVec::from_vec(ut_inputs).unwrap();
+
+          let mut boxes_to_spend: Vec<_> = inputs.into_iter().map(|(b,_)| b).collect();
+
+          let data_inputs = Some(
+              TxIoVec::from_vec(data_input_boxes
+                  .clone())
+                  .unwrap()
+                  .mapped(|b| DataInput{box_id: b.box_id()})
+          );
+
+          let expected_data_input_boxes = data_input_boxes.clone();
+          let expected_input_boxes = boxes_to_spend.clone();
+
+          // Reverse boxes for `UnsignedTransaction`
+          boxes_to_spend.reverse();
+          data_input_boxes.reverse();
+          let data_input_boxes = Some(TxIoVec::from_vec(data_input_boxes.clone()).unwrap());
+          let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone()).unwrap();
+          let spending_tx = UnsignedTransaction::new(
+              ut_inputs,
+              data_inputs,
+              TxIoVec::from_vec(vec![candidate]).unwrap(),
+          ).unwrap();
+          let tx_context = TransactionContext::new(
+              spending_tx,
+              boxes_to_spend,
+              data_input_boxes,
+          )
+          .unwrap();
+
+          let expected_data_input_boxes = Some(TxIoVec::from_vec(expected_data_input_boxes).unwrap().mapped(Rc::new));
+          let expected_input_boxes = TxIoVec::from_vec(expected_input_boxes).unwrap().mapped(Rc::new);
+          for i in 0..num_inputs {
+              let context = make_context(&force_any_val::<ErgoStateContext>(), &tx_context, i).unwrap();
+              assert_eq!(expected_data_input_boxes, context.data_inputs);
+              assert_eq!(expected_input_boxes, context.inputs);
+              assert_eq!(tx_context.spending_tx.inputs.as_vec()[i].box_id, context.self_box.box_id());
+          }
+        }
+    }
+
     #[test]
     fn test_proof_from_mainnet() {
         use crate::chain::transaction::Transaction;
diff --git a/ergotree-interpreter/Cargo.toml b/ergotree-interpreter/Cargo.toml
index 9565b6b1a..f0a42d725 100644
--- a/ergotree-interpreter/Cargo.toml
+++ b/ergotree-interpreter/Cargo.toml
@@ -27,6 +27,7 @@ derive_more = "0.99"
 num-traits = "0.2.14"
 proptest-derive = {version = "0.3.0", optional = true }
 base16 = "0.2.1"
+bounded-vec = { version = "^0.5.0", features=["serde"] }
 bytes = "1.1"
 num-bigint = "0.4.0"
 scorex_crypto_avltree = "0.1.0"
diff --git a/ergotree-interpreter/src/eval/coll_filter.rs b/ergotree-interpreter/src/eval/coll_filter.rs
index 0a24155e2..fce88488d 100644
--- a/ergotree-interpreter/src/eval/coll_filter.rs
+++ b/ergotree-interpreter/src/eval/coll_filter.rs
@@ -129,11 +129,21 @@ mod tests {
             .unwrap()
             .into();
             let ctx = Rc::new(ctx);
+            let expected: Vec<_> = ctx
+                .data_inputs
+                .clone()
+                .map_or(
+                     vec![],
+                     |d| d
+                         .as_vec()
+                         .iter()
+                         .cloned()
+                         .filter(| b| 1 <= b.value.as_i64())
+                         .collect()
+                );
             assert_eq!(
-                eval_out::<Vec<Rc<ErgoBox>>>(&expr, ctx.clone()),
-                ctx.data_inputs.clone()
-                    .into_iter()
-                    .filter(| b| 1 <= b.value.as_i64()).collect::<Vec<Rc<ErgoBox>>>()
+                eval_out::<Vec<Rc<ErgoBox>>>(&expr, ctx),
+                expected,
             );
         }
     }
diff --git a/ergotree-interpreter/src/eval/coll_fold.rs b/ergotree-interpreter/src/eval/coll_fold.rs
index 4bd030ee7..6b3e10fc1 100644
--- a/ergotree-interpreter/src/eval/coll_fold.rs
+++ b/ergotree-interpreter/src/eval/coll_fold.rs
@@ -126,9 +126,8 @@ mod tests {
             let ctx = Rc::new(ctx);
             assert_eq!(
                 eval_out::<i64>(&expr, ctx.clone()),
-                ctx.data_inputs
-                    .iter()
-                    .fold(0i64, |acc, b| acc + b.value.as_i64())
+                ctx.data_inputs.clone()
+                    .map_or(0i64, |d| d.iter().fold(0i64, |acc, b| acc + b.value.as_i64()))
             );
         }
 
diff --git a/ergotree-interpreter/src/eval/coll_map.rs b/ergotree-interpreter/src/eval/coll_map.rs
index 3b77586e6..9b76ada8b 100644
--- a/ergotree-interpreter/src/eval/coll_map.rs
+++ b/ergotree-interpreter/src/eval/coll_map.rs
@@ -72,6 +72,7 @@ mod tests {
     use std::rc::Rc;
 
     use crate::eval::context::Context;
+    use crate::eval::context::TxIoVec;
     use crate::eval::tests::eval_out;
     use ergotree_ir::mir::bin_op::ArithOp;
     use ergotree_ir::mir::bin_op::BinOp;
@@ -125,11 +126,18 @@ mod tests {
             .unwrap()
             .into();
             let ctx = Rc::new(ctx);
+            let output = {
+                let e = eval_out::<Vec<i64>>(&expr, ctx.clone());
+                if e.is_empty() {
+                  None
+                } else {
+                  Some(TxIoVec::from_vec(e).unwrap())
+                }
+            };
+
             assert_eq!(
-                eval_out::<Vec<i64>>(&expr, ctx.clone()),
-                ctx.data_inputs
-                    .iter()
-                    .map(| b| b.value.as_i64() + 1).collect::<Vec<i64>>()
+                output,
+                ctx.data_inputs.clone().map(|d| d.mapped(| b| b.value.as_i64() + 1))
             );
         }
 
diff --git a/ergotree-interpreter/src/eval/context.rs b/ergotree-interpreter/src/eval/context.rs
index 196cde0fe..4b6f35d56 100644
--- a/ergotree-interpreter/src/eval/context.rs
+++ b/ergotree-interpreter/src/eval/context.rs
@@ -1,10 +1,14 @@
 use std::rc::Rc;
 
 use crate::sigma_protocol::prover::ContextExtension;
+use bounded_vec::BoundedVec;
 use ergotree_ir::chain::ergo_box::ErgoBox;
 use ergotree_ir::chain::header::Header;
 use ergotree_ir::chain::preheader::PreHeader;
 
+/// BoundedVec type for Tx inputs, output_candidates and outputs
+pub type TxIoVec<T> = BoundedVec<T, 1, { u16::MAX as usize }>;
+
 /// Interpreter's context (blockchain state)
 #[derive(Debug)]
 pub struct Context {
@@ -15,9 +19,9 @@ pub struct Context {
     /// Spending transaction outputs
     pub outputs: Vec<Rc<ErgoBox>>,
     /// Spending transaction data inputs
-    pub data_inputs: Vec<Rc<ErgoBox>>,
+    pub data_inputs: Option<TxIoVec<Rc<ErgoBox>>>,
     /// Spending transaction inputs
-    pub inputs: Vec<Rc<ErgoBox>>,
+    pub inputs: TxIoVec<Rc<ErgoBox>>,
     /// Pre header of current block
     pub pre_header: PreHeader,
     /// Fixed number of last block headers in descending order (first header is the newest one)
@@ -37,11 +41,11 @@ impl Context {
 }
 
 #[cfg(feature = "arbitrary")]
+#[allow(clippy::unwrap_used)]
 mod arbitrary {
 
     use super::*;
-    use proptest::collection::vec;
-    use proptest::prelude::*;
+    use proptest::{collection::vec, option::of, prelude::*};
 
     impl Arbitrary for Context {
         type Parameters = ();
@@ -52,7 +56,7 @@ mod arbitrary {
                 any::<ErgoBox>(),
                 vec(any::<ErgoBox>(), 1..3),
                 vec(any::<ErgoBox>(), 1..3),
-                vec(any::<ErgoBox>(), 0..3),
+                of(vec(any::<ErgoBox>(), 1..3)),
                 any::<PreHeader>(),
                 any::<ContextExtension>(),
                 any::<[Header; 10]>(),
@@ -72,8 +76,11 @@ mod arbitrary {
                             height,
                             self_box: Rc::new(self_box),
                             outputs: outputs.into_iter().map(Rc::new).collect(),
-                            data_inputs: data_inputs.into_iter().map(Rc::new).collect(),
-                            inputs: inputs.into_iter().map(Rc::new).collect(),
+                            data_inputs: data_inputs.map(|v| {
+                                TxIoVec::from_vec(v.into_iter().map(Rc::new).collect()).unwrap()
+                            }),
+                            inputs: TxIoVec::from_vec(inputs.into_iter().map(Rc::new).collect())
+                                .unwrap(),
                             pre_header,
                             extension,
                             headers,
diff --git a/ergotree-interpreter/src/eval/global_vars.rs b/ergotree-interpreter/src/eval/global_vars.rs
index c9663ab40..125e33d6f 100644
--- a/ergotree-interpreter/src/eval/global_vars.rs
+++ b/ergotree-interpreter/src/eval/global_vars.rs
@@ -14,7 +14,7 @@ impl Evaluable for GlobalVars {
             GlobalVars::Height => Ok((ectx.ctx.height as i32).into()),
             GlobalVars::SelfBox => Ok(ectx.ctx.self_box.clone().into()),
             GlobalVars::Outputs => Ok(ectx.ctx.outputs.clone().into()),
-            GlobalVars::Inputs => Ok(ectx.ctx.inputs.clone().into()),
+            GlobalVars::Inputs => Ok(ectx.ctx.inputs.as_vec().clone().into()),
             GlobalVars::MinerPubKey => {
                 Ok(ectx.ctx.pre_header.miner_pk.sigma_serialize_bytes()?.into())
             }
@@ -68,7 +68,7 @@ mod tests {
         let ctx = Rc::new(force_any_val::<Context>());
         assert_eq!(
             eval_out::<Vec<Rc<ErgoBox>>>(&GlobalVars::Inputs.into(), ctx.clone()),
-            ctx.inputs
+            *ctx.inputs.as_vec()
         );
     }
 
diff --git a/ergotree-interpreter/src/eval/property_call.rs b/ergotree-interpreter/src/eval/property_call.rs
index ce83762bf..d47d0bc51 100644
--- a/ergotree-interpreter/src/eval/property_call.rs
+++ b/ergotree-interpreter/src/eval/property_call.rs
@@ -32,9 +32,10 @@ mod tests {
             .unwrap()
             .into();
         let ctx = Rc::new(force_any_val::<Context>());
-        assert_eq!(
-            eval_out::<Vec<Rc<ErgoBox>>>(&pc, ctx.clone()),
-            ctx.data_inputs
-        );
+        let expected = ctx
+            .data_inputs
+            .clone()
+            .map_or(vec![], |d| d.as_vec().clone());
+        assert_eq!(eval_out::<Vec<Rc<ErgoBox>>>(&pc, ctx), expected,);
     }
 }
diff --git a/ergotree-interpreter/src/eval/scontext.rs b/ergotree-interpreter/src/eval/scontext.rs
index 689cab5e4..1b12abdf8 100644
--- a/ergotree-interpreter/src/eval/scontext.rs
+++ b/ergotree-interpreter/src/eval/scontext.rs
@@ -19,9 +19,7 @@ pub(crate) static DATA_INPUTS_EVAL_FN: EvalFn = |_env, ctx, obj, _args| {
             .ctx
             .data_inputs
             .clone()
-            .into_iter()
-            .map(Value::CBox)
-            .collect(),
+            .map_or(vec![], |d| d.mapped(Value::CBox).as_vec().clone()),
         elem_tpe: SType::SBox,
     }))
 };
@@ -93,7 +91,7 @@ pub(crate) static LAST_BLOCK_UTXO_ROOT_HASH_EVAL_FN: EvalFn = |_env, ctx, obj, _
 #[cfg(feature = "arbitrary")]
 #[allow(clippy::unwrap_used, clippy::expect_used)]
 mod tests {
-    use crate::eval::context::Context;
+    use crate::eval::context::{Context, TxIoVec};
     use crate::eval::tests::eval_out;
     use ergotree_ir::chain::ergo_box::ErgoBox;
     use ergotree_ir::chain::header::Header;
@@ -108,7 +106,11 @@ mod tests {
     fn make_ctx_inputs_includes_self_box() -> Context {
         let ctx = force_any_val::<Context>();
         let self_box = force_any_val::<ErgoBox>();
-        let inputs = vec![force_any_val::<ErgoBox>().into(), self_box.clone().into()];
+        let inputs = TxIoVec::from_vec(vec![
+            force_any_val::<ErgoBox>().into(),
+            self_box.clone().into(),
+        ])
+        .unwrap();
         Context {
             height: 0u32,
             self_box: self_box.into(),


Test patch: