ergo_lib/wallet/
tx_context.rs

1//! Transaction context
2
3use std::collections::hash_map::Entry;
4use std::collections::HashMap;
5
6use crate::chain::ergo_state_context::ErgoStateContext;
7use crate::chain::transaction::ergo_transaction::{ErgoTransaction, TxValidationError};
8use crate::chain::transaction::{verify_tx_input_proof, Transaction, TransactionError};
9use crate::ergotree_ir::chain::ergo_box::BoxId;
10use ergotree_interpreter::eval::context::TxIoVec;
11use ergotree_interpreter::sigma_protocol::verifier::VerificationResult;
12use ergotree_ir::chain::ergo_box::box_value::BoxValue;
13use ergotree_ir::chain::ergo_box::{BoxTokens, ErgoBox};
14use ergotree_ir::chain::token::{TokenAmount, TokenId};
15use ergotree_ir::serialization::SigmaSerializable;
16use thiserror::Error;
17
18use super::signing::make_context;
19
20/// Transaction and an additional info required for signing or verification
21#[derive(PartialEq, Eq, Debug, Clone)]
22pub struct TransactionContext<T: ErgoTransaction> {
23    /// Unsigned transaction to sign
24    pub spending_tx: T,
25    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::inputs`]
26    boxes_to_spend: TxIoVec<ErgoBox>,
27    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::data_inputs`]
28    pub(crate) data_boxes: Option<TxIoVec<ErgoBox>>,
29    /// Stores the location of each BoxId in [`Self::boxes_to_spend`]
30    box_index: HashMap<BoxId, u16>,
31}
32
33impl<T: ErgoTransaction> TransactionContext<T> {
34    /// New TransactionContext
35    pub fn new(
36        spending_tx: T,
37        boxes_to_spend: Vec<ErgoBox>,
38        data_boxes: Vec<ErgoBox>,
39    ) -> Result<Self, TransactionContextError> {
40        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend).map_err(|e| match e {
41            bounded_vec::BoundedVecOutOfBounds::LowerBoundError { .. } => {
42                TransactionContextError::NoInputBoxes
43            }
44            bounded_vec::BoundedVecOutOfBounds::UpperBoundError { got, .. } => {
45                TransactionContextError::TooManyInputBoxes(got)
46            }
47        })?;
48        let data_boxes_len = data_boxes.len();
49        let data_boxes = if !data_boxes.is_empty() {
50            Some(
51                TxIoVec::from_vec(data_boxes)
52                    .map_err(|_| TransactionContextError::TooManyDataInputBoxes(data_boxes_len))?,
53            )
54        } else {
55            None
56        };
57
58        let box_index: HashMap<BoxId, u16> = boxes_to_spend
59            .iter()
60            .enumerate()
61            .map(|(i, b)| (b.box_id(), i as u16))
62            .collect();
63        for (i, unsigned_input) in spending_tx.inputs_ids().enumerate() {
64            if !box_index.contains_key(&unsigned_input) {
65                return Err(TransactionContextError::InputBoxNotFound(i));
66            }
67        }
68
69        if let Some(data_inputs) = spending_tx.data_inputs().as_ref() {
70            if let Some(data_boxes) = data_boxes.as_ref() {
71                let data_box_index: HashMap<BoxId, u16> = data_boxes
72                    .iter()
73                    .enumerate()
74                    .map(|(i, b)| (b.box_id(), i as u16))
75                    .collect();
76                for (i, data_input) in data_inputs.iter().enumerate() {
77                    if !data_box_index.contains_key(&data_input.box_id) {
78                        return Err(TransactionContextError::DataInputBoxNotFound(i));
79                    }
80                }
81            } else {
82                return Err(TransactionContextError::DataInputBoxNotFound(0));
83            }
84        }
85        Ok(TransactionContext {
86            spending_tx,
87            boxes_to_spend,
88            data_boxes,
89            box_index,
90        })
91    }
92
93    /// Returns box with given id, if it exists.
94    pub fn get_input_box(&self, box_id: &BoxId) -> Option<&ErgoBox> {
95        self.box_index
96            .get(box_id)
97            .and_then(|&idx| self.boxes_to_spend.get(idx as usize))
98    }
99}
100
101impl TransactionContext<Transaction> {
102    /// Verify transaction using blockchain parameters
103    // TODO: costing
104    // This is based on validateStateful() in Ergo: https://github.com/ergoplatform/ergo/blob/48239ef98ced06617dc21a0eee5670235e362933/ergo-core/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala#L357
105    pub fn validate(&self, state_context: &ErgoStateContext) -> Result<(), TxValidationError> {
106        // Check that input sum does not overflow
107        let input_sum = BoxValue::new(
108            self.boxes_to_spend
109                .iter()
110                .map(|b| b.value.as_u64())
111                .sum::<u64>(),
112        )
113        .map_err(|_| TxValidationError::InputSumOverflow)?;
114        // Check that output sum does not overflow and is equal to ERG amount in inputs
115        let output_sum = self
116            .spending_tx
117            .outputs
118            .iter()
119            .map(|b| b.value.as_u64())
120            .sum();
121        if *input_sum.as_u64() != output_sum {
122            return Err(TxValidationError::ErgPreservationError(
123                *input_sum.as_u64(),
124                output_sum,
125            ));
126        }
127
128        // Monotonic Box creation happens after v3
129        let max_creation_height = if state_context.pre_header.version <= 2 {
130            0
131        } else {
132            #[allow(clippy::unwrap_used)] // Unwrap is valid here since inputs can not be empty
133            self.boxes_to_spend
134                .iter()
135                .map(|b| b.creation_height)
136                .max()
137                .unwrap()
138        };
139        // Check that outputs are not dust and aren't created in future
140        for output in &self.spending_tx.outputs {
141            verify_output(state_context, output, max_creation_height)?;
142        }
143
144        let in_assets = extract_assets(self.boxes_to_spend.iter().map(|b| &b.tokens))?;
145        let out_assets = extract_assets(self.spending_tx.outputs.iter().map(|b| &b.tokens))?;
146        verify_assets(self.spending_tx.inputs_ids(), in_assets, out_assets)?;
147        // Verify input proofs. This is usually the most expensive check so it's done last
148        let bytes_to_sign = self.spending_tx.bytes_to_sign()?;
149        let mut context = make_context(state_context, self, 0)?;
150        for input_idx in 0..self.spending_tx.inputs.len() {
151            if let res @ VerificationResult { result: false, .. } =
152                verify_tx_input_proof(self, &mut context, state_context, input_idx, &bytes_to_sign)?
153            {
154                return Err(TxValidationError::ReducedToFalse(input_idx, res));
155            }
156        }
157        Ok(())
158    }
159}
160
161fn verify_output(
162    state_context: &ErgoStateContext,
163    output: &ErgoBox,
164    max_creation_height: u32,
165) -> Result<(), TxValidationError> {
166    let box_size = output.sigma_serialize_bytes()?.len() as u64;
167    let script_size = output.script_bytes()?.len();
168    let block_version = state_context.pre_header.version;
169    // Check that output is not dust
170    let minimum_value = box_size * state_context.parameters.min_value_per_byte() as u64;
171    if *output.value.as_u64() < minimum_value {
172        return Err(TxValidationError::DustOutput(
173            output.box_id(),
174            output.value,
175            minimum_value,
176        ));
177    }
178    // Check that height does not exceed maximum height. Note that heights can be potentially negative in V1
179    if output.creation_height as i32 > state_context.pre_header.height as i32 {
180        return Err(TxValidationError::InvalidHeightError(
181            output.creation_height,
182        ));
183    }
184    if output.creation_height < max_creation_height {
185        return Err(TxValidationError::MonotonicHeightError(
186            output.creation_height,
187            max_creation_height,
188        ));
189    }
190    // Negative output heights were allowed in V1. sigma-rust always stores heights as unsigned integers
191    if block_version != 1 && output.creation_height & (1 << 31) != 0 {
192        return Err(TxValidationError::NegativeHeight);
193    }
194    if box_size as usize > ErgoBox::MAX_BOX_SIZE {
195        return Err(TxValidationError::BoxSizeExceeded(box_size as usize));
196    }
197    if script_size > ErgoBox::MAX_SCRIPT_SIZE {
198        return Err(TxValidationError::ScriptSizeExceeded(script_size));
199    }
200    Ok(())
201}
202
203// Extract all of the assets in a collection of boxes for transaction validation
204fn extract_assets<'a, I: Iterator<Item = &'a Option<BoxTokens>>>(
205    mut boxes: I,
206) -> Result<HashMap<TokenId, TokenAmount>, TxValidationError> {
207    boxes.try_fold(
208        HashMap::new(),
209        |mut asset_map: HashMap<TokenId, TokenAmount>, tokens| {
210            tokens
211                .as_ref()
212                .into_iter()
213                .flatten()
214                .try_for_each(|token| {
215                    match asset_map.entry(token.token_id) {
216                        Entry::Occupied(mut occ) => {
217                            *occ.get_mut() = occ.get().checked_add(&token.amount)?;
218                        }
219                        Entry::Vacant(vac) => {
220                            vac.insert(token.amount);
221                        }
222                    }
223                    Ok::<(), TxValidationError>(())
224                })?;
225            Ok(asset_map)
226        },
227    )
228}
229
230fn verify_assets(
231    mut inputs: impl Iterator<Item = BoxId>,
232    in_assets: HashMap<TokenId, TokenAmount>,
233    out_assets: HashMap<TokenId, TokenAmount>,
234) -> Result<(), TxValidationError> {
235    // If this transaction mints a new token, it's token ID must be the ID of the first box being spent
236    #[allow(clippy::unwrap_used)]
237    // Inputs size is already validated so it must be of atleast size 1
238    let new_token_id: TokenId = inputs.next().unwrap().into();
239    for (&out_token_id, &out_amount) in &out_assets {
240        if let Some(&in_amount) = in_assets.get(&out_token_id) {
241            // Check that Transaction is not creating tokens out of thin air
242            if in_amount < out_amount {
243                return Err(TxValidationError::TokenPreservationError {
244                    token_id: out_token_id,
245                    in_amount: in_amount.into(),
246                    out_amount: out_amount.into(),
247                    new_token_id,
248                });
249            }
250        } else if out_token_id != new_token_id {
251            //minting a new token. Token amount checks are handled by the TokenAmount newtype and not needed here
252            return Err(TxValidationError::TokenPreservationError {
253                token_id: out_token_id,
254                in_amount: 0,
255                out_amount: out_amount.into(),
256                new_token_id,
257            });
258        }
259    }
260    Ok(())
261}
262
263/// Transaction context errors
264#[derive(Error, Debug)]
265pub enum TransactionContextError {
266    /// Transaction error
267    #[error("Transaction error: {0}")]
268    TransactionError(#[from] TransactionError),
269    /// No input boxes (boxes_to_spend is empty)
270    #[error("No input boxes")]
271    NoInputBoxes,
272    /// Too many input boxes
273    #[error("Too many input boxes: {0}")]
274    TooManyInputBoxes(usize),
275    /// Input box not found
276    #[error("Input box not found: {0}")]
277    InputBoxNotFound(usize),
278    /// Too many data input boxes
279    #[error("Too many data input boxes: {0}")]
280    TooManyDataInputBoxes(usize),
281    /// Data input box not found
282    #[error("Data input box not found: {0}")]
283    DataInputBoxNotFound(usize),
284}
285
286#[cfg(test)]
287#[allow(clippy::unwrap_used, clippy::panic)]
288mod test {
289    use std::collections::HashMap;
290
291    use ergotree_interpreter::eval::context::TxIoVec;
292    use ergotree_interpreter::sigma_protocol::prover::{ContextExtension, ProofBytes};
293    use ergotree_ir::chain::ergo_box::arbitrary::ArbBoxParameters;
294    use ergotree_ir::chain::ergo_box::box_value::BoxValue;
295    use ergotree_ir::chain::ergo_box::{
296        BoxTokens, ErgoBox, ErgoBoxCandidate, NonMandatoryRegisters,
297    };
298    use ergotree_ir::chain::token::arbitrary::ArbTokenIdParam;
299    use ergotree_ir::chain::token::{Token, TokenAmount, TokenId};
300    use ergotree_ir::ergo_tree::{ErgoTree, ErgoTreeHeader};
301    use ergotree_ir::mir::constant::{Constant, Literal};
302    use ergotree_ir::mir::expr::Expr;
303    use proptest::prelude::*;
304    use proptest::strategy::Strategy;
305    use proptest::test_runner::TestRng;
306    use sigma_test_util::{force_any_val, force_any_val_with};
307
308    use crate::chain::ergo_state_context::ErgoStateContext;
309    use crate::chain::parameters::Parameters;
310    use crate::chain::transaction::ergo_transaction::{ErgoTransaction, TxValidationError};
311    use crate::chain::transaction::prover_result::ProverResult;
312    use crate::chain::transaction::unsigned::UnsignedTransaction;
313    use crate::chain::transaction::{Input, Transaction, UnsignedInput};
314    use crate::wallet::Wallet;
315
316    use super::TransactionContext;
317
318    // Disperse token_count tokens across inputs
319    fn disperse_tokens(inputs: u16, token_count: u8) -> Vec<Option<BoxTokens>> {
320        let mut token_distribution = vec![vec![]; inputs as usize];
321        for i in 0..token_count {
322            let token = force_any_val_with::<Token>(ArbTokenIdParam::Arbitrary);
323            token_distribution[(i as usize) % inputs as usize].push(token);
324        }
325        token_distribution
326            .into_iter()
327            .map(BoxTokens::from_vec)
328            .map(Result::ok)
329            .collect()
330    }
331    fn gen_boxes(
332        min_tokens: u8,
333        max_tokens: u8,
334        min_inputs: u16,
335        max_inputs: u16,
336        ergotree_gen: impl Strategy<Value = ErgoTree>,
337        height_gen: Option<BoxedStrategy<u32>>,
338    ) -> impl Strategy<Value = Vec<ErgoBox>> {
339        (
340            min_inputs..=max_inputs,
341            min_tokens..=max_tokens,
342            ergotree_gen,
343            height_gen.clone().unwrap_or_else(|| Just(0).boxed()),
344        )
345            .prop_flat_map(
346                |(input_count, assets_count, proposition, creation_height)| {
347                    let tokens = disperse_tokens(input_count, assets_count);
348                    tokens
349                        .into_iter()
350                        .map(move |tokens| {
351                            let box_params = ArbBoxParameters {
352                                value_range: (1000000..100000000).into(),
353                                ergo_tree: Just(proposition.clone()).boxed(),
354                                creation_height: Just(creation_height).boxed(),
355                                tokens: Just(tokens).boxed(),
356                                ..Default::default()
357                            };
358                            ErgoBox::arbitrary_with(box_params)
359                        })
360                        .collect::<Vec<_>>()
361                },
362            )
363    }
364    fn valid_unsigned_transaction_from_boxes(
365        mut rng: TestRng,
366        boxes: &[ErgoBox],
367        issue_new_token: bool,
368        output_prop: ErgoTree,
369        _data_boxes: &[ErgoBox],
370    ) -> UnsignedTransaction {
371        let input_sum = boxes.iter().map(|b| *b.value.as_u64()).sum::<u64>();
372        assert!(input_sum > *BoxValue::SAFE_USER_MIN.as_u64());
373
374        let mut assets_map: HashMap<TokenId, TokenAmount> = boxes
375            .iter()
376            .flat_map(|b| b.tokens.clone().into_iter().flatten())
377            .map(|token| (token.token_id, token.amount))
378            .collect();
379        if issue_new_token {
380            assets_map.insert(
381                boxes[0].box_id().into(),
382                rng.gen_range(1..=i64::MAX as u64).try_into().unwrap(),
383            );
384        }
385
386        let parameters = Parameters::default();
387        let sufficient_amount =
388            ErgoBox::MAX_BOX_SIZE as u64 * parameters.min_value_per_byte() as u64;
389        let max_outputs = std::cmp::min(i16::MAX as u16, (input_sum / sufficient_amount) as u16);
390        let outputs = std::cmp::min(
391            max_outputs,
392            std::cmp::max(boxes.len() + 1, rng.gen_range(0..boxes.len() * 2)) as u16,
393        );
394        assert!(outputs > 0);
395        assert!(sufficient_amount * (outputs as u64) <= input_sum);
396        let mut output_preamounts = vec![sufficient_amount; outputs as usize];
397        let mut remainder = input_sum - sufficient_amount * outputs as u64;
398        while remainder > 0 {
399            let idx = rng.gen_range(0..output_preamounts.len());
400            if remainder < input_sum / boxes.len() as u64 {
401                output_preamounts[idx] = output_preamounts[idx].checked_add(remainder).unwrap();
402                remainder = 0;
403            } else {
404                let val = rng.gen_range(0..=remainder);
405                output_preamounts[idx] = output_preamounts[idx].checked_add(val).unwrap();
406                remainder -= val;
407            }
408        }
409
410        let mut token_amounts: Vec<HashMap<TokenId, u64>> = vec![HashMap::new(); outputs as usize];
411        let mut available_token_slots = (outputs * 255) as usize;
412        while !assets_map.is_empty() && available_token_slots > 0 {
413            let cur = assets_map
414                .iter()
415                .map(|(&token_id, &token_amount)| (token_id, token_amount))
416                .next()
417                .unwrap();
418            let out_idx = loop {
419                let idx = rng.gen_range(0..token_amounts.len());
420                if token_amounts[idx].len() < 255 {
421                    break idx;
422                }
423            };
424            let contains = token_amounts[out_idx].contains_key(&cur.0);
425
426            let amount = if *cur.1.as_u64() == 1
427                || (available_token_slots < assets_map.len() * 2 && !contains)
428                || rng.gen()
429            {
430                *cur.1.as_u64()
431            } else {
432                rng.gen_range(1..=*cur.1.as_u64())
433            };
434            if amount == *cur.1.as_u64() {
435                assets_map.remove(&cur.0);
436            } else {
437                assets_map.entry(cur.0).and_modify(|amt| {
438                    *amt = amt
439                        .checked_sub(&TokenAmount::try_from(amount).unwrap())
440                        .unwrap()
441                });
442            }
443            token_amounts[out_idx]
444                .entry(cur.0)
445                .and_modify(|token_amount| *token_amount += amount)
446                .or_insert_with(|| {
447                    available_token_slots -= 1;
448                    amount
449                });
450        }
451        let output_boxes = output_preamounts
452            .into_iter()
453            .zip(token_amounts)
454            .map(|(amount, tokens)| -> (u64, Option<BoxTokens>) {
455                (
456                    amount,
457                    tokens
458                        .into_iter()
459                        .map(|(token_id, token_amount)| {
460                            Token::from((token_id, TokenAmount::try_from(token_amount).unwrap()))
461                        })
462                        .collect::<Vec<_>>()
463                        .try_into()
464                        .ok(),
465                )
466            })
467            .map(|(amount, tokens)| ErgoBoxCandidate {
468                value: BoxValue::new(amount).unwrap(),
469                ergo_tree: output_prop.clone(),
470                tokens,
471                additional_registers: NonMandatoryRegisters::empty(),
472                creation_height: 0,
473            })
474            .collect();
475        UnsignedTransaction::new_from_vec(
476            boxes
477                .iter()
478                .map(|b| UnsignedInput::new(b.box_id(), ContextExtension::empty()))
479                .collect(),
480            vec![],
481            output_boxes,
482        )
483        .unwrap()
484    }
485    fn valid_transaction_from_boxes(
486        rng: TestRng,
487        boxes: Vec<ErgoBox>,
488        issue_new_token: bool,
489        output_prop: ErgoTree,
490        data_boxes: Vec<ErgoBox>,
491    ) -> Transaction {
492        let unsigned_tx = valid_unsigned_transaction_from_boxes(
493            rng,
494            &boxes,
495            issue_new_token,
496            output_prop,
497            &data_boxes,
498        );
499        let tx_context =
500            TransactionContext::new(unsigned_tx.clone(), boxes.clone(), data_boxes).unwrap();
501        let wallet = Wallet::from_secrets(vec![]);
502        let state_context = force_any_val();
503        // Attempt to sign a transaction. If signing fails because script reduces to false or prover doesn't know some secret then return an invalid transaction
504        wallet
505            .sign_transaction(tx_context, &state_context, None)
506            .or_else(|_| {
507                Transaction::new(
508                    TxIoVec::from_vec(
509                        boxes
510                            .iter()
511                            .map(|b| Input {
512                                box_id: b.box_id(),
513                                spending_proof: ProverResult {
514                                    proof: ProofBytes::Empty,
515                                    extension: ContextExtension::empty(),
516                                },
517                            })
518                            .collect(),
519                    )
520                    .unwrap(),
521                    unsigned_tx.data_inputs,
522                    unsigned_tx.output_candidates,
523                )
524            })
525            .unwrap()
526    }
527    fn valid_transaction_gen_with_tree(
528        tree: ErgoTree,
529    ) -> impl Strategy<Value = (Vec<ErgoBox>, Transaction)> {
530        let box_generator = gen_boxes(1, 100, 1, 100, Just(tree.clone()), None);
531        (box_generator, bool::arbitrary()).prop_perturb(move |(boxes, issue_new_token), rng| {
532            (
533                boxes.clone(),
534                valid_transaction_from_boxes(rng, boxes, issue_new_token, tree.clone(), vec![]),
535            )
536        })
537    }
538
539    fn valid_transaction_generator() -> impl Strategy<Value = (Vec<ErgoBox>, Transaction)> {
540        let true_tree = ErgoTree::new(
541            ErgoTreeHeader::v0(true),
542            &Expr::Const(Constant {
543                tpe: ergotree_ir::types::stype::SType::SBoolean,
544                v: Literal::Boolean(true),
545            }),
546        )
547        .unwrap();
548        valid_transaction_gen_with_tree(true_tree)
549    }
550
551    fn update_asset<F: FnOnce(TokenAmount) -> TokenAmount>(
552        transaction: &mut Transaction,
553        boxes: &[ErgoBox],
554        f: F,
555    ) {
556        for output in transaction.outputs.iter_mut() {
557            if let Some(token) = output
558                .tokens
559                .iter_mut()
560                .flatten()
561                .find(|t| t.token_id != boxes[0].box_id().into())
562            {
563                token.amount = f(token.amount);
564                break;
565            }
566        }
567    }
568
569    proptest! {
570    #[test]
571    // Test that a valid transaction is valid
572    fn test_valid_transaction((boxes, tx) in valid_transaction_generator()) {
573        let state_context = force_any_val();
574        let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
575        tx_context.validate(&state_context).unwrap();
576    }
577    #[test]
578    fn test_ergo_preservation((mut boxes, mut tx) in valid_transaction_generator(), positive_delta: bool, change_output: bool) {
579        let state_context = force_any_val();
580
581        let box_value = if change_output {
582            let slice: &mut [ErgoBox] = tx.outputs.as_mut();
583            &mut slice[0].value
584        }
585        else {
586            &mut boxes[0].value
587        };
588        if positive_delta {
589            *box_value = box_value.checked_add(&BoxValue::SAFE_USER_MIN).unwrap();
590        }
591        else {
592            *box_value = BoxValue::try_from(box_value.as_u64() - 1).unwrap();
593        }
594
595        assert!(tx.validate_stateless().is_ok());
596
597        let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
598        match tx_context.validate(&state_context) {
599            Err(TxValidationError::ErgPreservationError(_, _)) => {},
600            e => panic!("Expected validation to fail got {e:?}")
601        }
602    }
603    #[test]
604    fn test_zero_asset_creation((boxes, mut tx) in valid_transaction_generator()) {
605        let state_context = force_any_val();
606        update_asset(&mut tx, &boxes, |amount| amount.checked_add(&TokenAmount::MIN).unwrap());
607        assert!(tx.validate_stateless().is_ok());
608
609        let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
610        match tx_context.validate(&state_context) {
611            Err(TxValidationError::TokenPreservationError { .. } ) => {},
612            other => panic!("Expected validation to fail, got {other:?}")
613        }
614    }
615    #[test]
616    fn test_asset_preservation((boxes, mut tx) in valid_transaction_generator()) {
617        let state_context = force_any_val();
618        update_asset(&mut tx, &boxes, |amount| amount.checked_add(&TokenAmount::MIN).unwrap());
619        assert!(tx.validate_stateless().is_ok());
620
621        let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
622        match tx_context.validate(&state_context) {
623            Err(TxValidationError::TokenPreservationError { .. } ) => {},
624            other => panic!("Expected validation to fail, got {other:?}")
625        }
626    }
627    }
628    // Test that unspendable boxes can't be included in a transaction
629    // TODO: When sigma-rust lands support for storage rent transactions, there should be a test that successfully passes validation when box is old enough
630    #[test]
631    fn test_false_proposition() {
632        let state_context = force_any_val();
633        let false_tree = ErgoTree::new(
634            ErgoTreeHeader::v0(true),
635            &Expr::Const(Constant {
636                tpe: ergotree_ir::types::stype::SType::SBoolean,
637                v: Literal::Boolean(false),
638            }),
639        )
640        .unwrap();
641        proptest!(|((boxes, tx) in valid_transaction_gen_with_tree(false_tree))| {
642            assert!(tx.validate_stateless().is_ok());
643
644            let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
645            match tx_context.validate(&state_context) {
646                Err(TxValidationError::ReducedToFalse(_, _)) => {},
647                other => panic!("Expected validation to fail, got {other:?}")
648            }
649        });
650    }
651    #[test]
652    fn test_monotonic_box_creation() {
653        let true_tree = ErgoTree::new(
654            ErgoTreeHeader::v0(true),
655            &Expr::Const(Constant {
656                tpe: ergotree_ir::types::stype::SType::SBoolean,
657                v: Literal::Boolean(true),
658            }),
659        )
660        .unwrap();
661
662        let state_context_tx_gen = |tx: &Transaction, version| {
663            let height = tx
664                .output_candidates
665                .iter()
666                .map(|b| b.creation_height)
667                .max()
668                .unwrap();
669            dbg!(height);
670            let mut state_context: ErgoStateContext = force_any_val();
671            state_context.pre_header.height = height;
672            state_context.pre_header.version = version;
673            state_context
674        };
675        let box_gen = gen_boxes(
676            5,
677            10,
678            5,
679            10,
680            Just(true_tree.clone()),
681            Some((0..i32::MAX as u32).boxed()),
682        );
683        // Generate a list of boxes. If monotonic_valid is true then monotonic height validation will pass, otherwise it will fail in tests
684        let tx_gen =
685            (box_gen, bool::arbitrary()).prop_perturb(|(boxes, monotonic_valid), mut rng| {
686                let max_height = boxes.iter().map(|b| b.creation_height).max().unwrap();
687                let mut unsigned_tx = valid_unsigned_transaction_from_boxes(
688                    rng.clone(),
689                    &boxes,
690                    true,
691                    true_tree.clone(),
692                    &[],
693                );
694                if monotonic_valid {
695                    unsigned_tx
696                        .output_candidates
697                        .iter_mut()
698                        .for_each(|b| b.creation_height = max_height + rng.gen_range(1..1000));
699                } else {
700                    unsigned_tx.output_candidates.iter_mut().for_each(|b| {
701                        b.creation_height = max_height.saturating_sub(rng.gen_range(1..1000))
702                    });
703                }
704                let wallet = Wallet::from_secrets(vec![]);
705                let state_context = force_any_val();
706                let tx_context =
707                    TransactionContext::new(unsigned_tx, boxes.clone(), vec![]).unwrap();
708                let signed_tx = wallet
709                    .sign_transaction(tx_context, &state_context, None)
710                    .unwrap();
711                (boxes, signed_tx, monotonic_valid)
712            });
713        proptest!(|((boxes, tx, monotonic_valid) in tx_gen)| {
714            assert!(tx.validate_stateless().is_ok());
715
716            // For blocks V1 and V2 monotonic height rule is not respected.
717            let context1 = state_context_tx_gen(&tx, 1);
718            let context2 = state_context_tx_gen(&tx, 2);
719            // V3 enforces monotonic height rule, thus validation should fail if !monotonic_valid
720            let context3 = state_context_tx_gen(&tx, 3);
721            let tx_context = TransactionContext::new(tx, boxes, vec![]).unwrap();
722            match tx_context.validate(&context1) {
723                Ok(_) => {},
724                other => panic!("Expected validation to succeed, got {other:?}")
725            }
726            match tx_context.validate(&context2) {
727                Ok(_) => {},
728                other => panic!("Expected validation to succeed, got {other:?}")
729            }
730            match (monotonic_valid, tx_context.validate(&context3)) {
731                (true, Ok(())) => {},
732                (false, Err(TxValidationError::MonotonicHeightError(_, _))) => {},
733                other => panic!("Expected validation to fail, got {other:?}")
734            }
735        });
736    }
737}