1use 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#[derive(PartialEq, Eq, Debug, Clone)]
22pub struct TransactionContext<T: ErgoTransaction> {
23 pub spending_tx: T,
25 boxes_to_spend: TxIoVec<ErgoBox>,
27 pub(crate) data_boxes: Option<TxIoVec<ErgoBox>>,
29 box_index: HashMap<BoxId, u16>,
31}
32
33impl<T: ErgoTransaction> TransactionContext<T> {
34 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 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 pub fn validate(&self, state_context: &ErgoStateContext) -> Result<(), TxValidationError> {
106 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 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 let max_creation_height = if state_context.pre_header.version <= 2 {
130 0
131 } else {
132 #[allow(clippy::unwrap_used)] self.boxes_to_spend
134 .iter()
135 .map(|b| b.creation_height)
136 .max()
137 .unwrap()
138 };
139 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 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 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 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 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
203fn 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 #[allow(clippy::unwrap_used)]
237 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 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 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#[derive(Error, Debug)]
265pub enum TransactionContextError {
266 #[error("Transaction error: {0}")]
268 TransactionError(#[from] TransactionError),
269 #[error("No input boxes")]
271 NoInputBoxes,
272 #[error("Too many input boxes: {0}")]
274 TooManyInputBoxes(usize),
275 #[error("Input box not found: {0}")]
277 InputBoxNotFound(usize),
278 #[error("Too many data input boxes: {0}")]
280 TooManyDataInputBoxes(usize),
281 #[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 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 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 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]
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 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 let context1 = state_context_tx_gen(&tx, 1);
718 let context2 = state_context_tx_gen(&tx, 2);
719 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}