• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ergoplatform / sigma-rust / 8953175335

04 May 2024 08:51PM UTC coverage: 80.473% (+0.1%) from 80.331%
8953175335

Pull #736

github

web-flow
Merge 0fdf2d258 into 57a105462
Pull Request #736: Transaction Validation

165 of 228 new or added lines in 15 files covered. (72.37%)

8 existing lines in 2 files now uncovered.

10723 of 13325 relevant lines covered (80.47%)

3.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.57
/ergo-lib/src/wallet/tx_context.rs
1
//! Transaction context
2

3
use std::collections::hash_map::Entry;
4
use std::collections::HashMap;
5

6
use crate::chain::ergo_state_context::ErgoStateContext;
7
use crate::chain::transaction::ergo_transaction::{ErgoTransaction, TxValidationError};
8
use crate::chain::transaction::{verify_tx_input_proof, Transaction, TransactionError};
9
use crate::ergotree_ir::chain::ergo_box::BoxId;
10
use ergotree_interpreter::eval::context::TxIoVec;
11
use ergotree_interpreter::sigma_protocol::verifier::VerificationResult;
12
use ergotree_ir::chain::ergo_box::box_value::BoxValue;
13
use ergotree_ir::chain::ergo_box::{BoxTokens, ErgoBox};
14
use ergotree_ir::chain::token::{TokenAmount, TokenId};
15
use ergotree_ir::serialization::SigmaSerializable;
16
use thiserror::Error;
17

18
/// Transaction and an additional info required for signing or verification
19
#[derive(PartialEq, Eq, Debug, Clone)]
20
pub struct TransactionContext<T: ErgoTransaction> {
21
    /// Unsigned transaction to sign
22
    pub spending_tx: T,
23
    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::inputs`]
24
    boxes_to_spend: TxIoVec<ErgoBox>,
25
    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::data_inputs`]
26
    pub(crate) data_boxes: Option<TxIoVec<ErgoBox>>,
27
    /// Stores the location of each BoxId in [`Self::boxes_to_spend`]
28
    box_index: HashMap<BoxId, u16>,
29
}
30

31
impl<T: ErgoTransaction> TransactionContext<T> {
32
    /// New TransactionContext
33
    pub fn new(
2✔
34
        spending_tx: T,
35
        boxes_to_spend: Vec<ErgoBox>,
36
        data_boxes: Vec<ErgoBox>,
37
    ) -> Result<Self, TransactionContextError> {
38
        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend).map_err(|e| match e {
4✔
39
            bounded_vec::BoundedVecOutOfBounds::LowerBoundError { .. } => {
×
40
                TransactionContextError::NoInputBoxes
×
41
            }
42
            bounded_vec::BoundedVecOutOfBounds::UpperBoundError { got, .. } => {
×
43
                TransactionContextError::TooManyInputBoxes(got)
×
44
            }
45
        })?;
46
        let data_boxes_len = data_boxes.len();
4✔
47
        let data_boxes = if !data_boxes.is_empty() {
5✔
48
            Some(
49
                TxIoVec::from_vec(data_boxes)
2✔
50
                    .map_err(|_| TransactionContextError::TooManyDataInputBoxes(data_boxes_len))?,
1✔
51
            )
52
        } else {
53
            None
2✔
54
        };
55

56
        let box_index: HashMap<BoxId, u16> = boxes_to_spend
4✔
57
            .iter()
58
            .enumerate()
59
            .map(|(i, b)| (b.box_id(), i as u16))
4✔
60
            .collect();
61
        for (i, unsigned_input) in spending_tx.inputs_ids().iter().enumerate() {
4✔
62
            if !box_index.contains_key(unsigned_input) {
4✔
NEW
63
                return Err(TransactionContextError::InputBoxNotFound(i));
×
64
            }
65
        }
66

67
        if let Some(data_inputs) = spending_tx.data_inputs().as_ref() {
4✔
68
            if let Some(data_boxes) = data_boxes.as_ref() {
2✔
69
                let data_box_index: HashMap<BoxId, u16> = data_boxes
2✔
70
                    .iter()
71
                    .enumerate()
72
                    .map(|(i, b)| (b.box_id(), i as u16))
2✔
73
                    .collect();
74
                for (i, data_input) in data_inputs.iter().enumerate() {
3✔
75
                    if !data_box_index.contains_key(&data_input.box_id) {
1✔
76
                        return Err(TransactionContextError::DataInputBoxNotFound(i));
×
77
                    }
78
                }
79
            } else {
80
                return Err(TransactionContextError::DataInputBoxNotFound(0));
×
81
            }
82
        }
83
        Ok(TransactionContext {
2✔
84
            spending_tx,
2✔
85
            boxes_to_spend,
2✔
86
            data_boxes,
2✔
87
            box_index,
2✔
88
        })
89
    }
90

91
    /// Returns box with given id, if it exists.
92
    pub fn get_input_box(&self, box_id: &BoxId) -> Option<ErgoBox> {
2✔
93
        self.box_index
4✔
NEW
94
            .get(box_id)
×
95
            .and_then(|&idx| self.boxes_to_spend.get(idx as usize))
6✔
96
            .cloned()
97
    }
98
}
99

100
impl TransactionContext<Transaction> {
101
    /// Verify transaction using blockchain parameters
102
    // TODO: costing
103
    // 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
104
    pub fn validate(&self, state_context: &ErgoStateContext) -> Result<(), TxValidationError> {
1✔
105
        // Check that input sum does not overflow
106
        let input_sum = BoxValue::new(
107
            self.boxes_to_spend
1✔
108
                .iter()
109
                .map(|b| b.value.as_u64())
2✔
110
                .sum::<u64>(),
111
        )
NEW
112
        .map_err(|_| TxValidationError::InputSumOverflow)?;
×
113
        // Check that output sum does not overflow and is equal to ERG amount in inputs
114
        let output_sum = self
1✔
115
            .spending_tx
116
            .outputs
117
            .iter()
118
            .map(|b| b.value.as_u64())
2✔
119
            .sum();
120
        if *input_sum.as_u64() != output_sum {
1✔
121
            return Err(TxValidationError::ErgPreservationError(
1✔
122
                *input_sum.as_u64(),
1✔
123
                output_sum,
124
            ));
125
        }
126

127
        // Monotonic Box creation happens after v3
128
        let max_creation_height = if state_context.pre_header.version <= 2 {
1✔
129
            0
1✔
130
        } else {
131
            #[allow(clippy::unwrap_used)] // Unwrap is valid here since inputs can not be empty
132
            self.boxes_to_spend
1✔
133
                .iter()
134
                .map(|b| b.creation_height)
2✔
135
                .max()
136
                .unwrap()
137
        };
138
        // Check that outputs are not dust and aren't created in future
139
        for output in &self.spending_tx.outputs {
2✔
140
            verify_output(state_context, output, max_creation_height)?;
2✔
141
        }
142

143
        let in_assets = extract_assets(self.boxes_to_spend.iter().map(|b| &b.tokens))?;
4✔
144
        let out_assets = extract_assets(self.spending_tx.outputs.iter().map(|b| &b.tokens))?;
4✔
145
        verify_assets(
146
            self.spending_tx.inputs_ids().as_slice(),
2✔
147
            in_assets,
1✔
148
            out_assets,
1✔
149
        )?;
150
        // Verify input proofs. This is usually the most expensive check so it's done last
151
        let bytes_to_sign = self.spending_tx.bytes_to_sign()?;
1✔
152
        for input_idx in 0..self.spending_tx.inputs.len() {
3✔
153
            if let res @ VerificationResult { result: false, .. } =
2✔
154
                verify_tx_input_proof(self, state_context, input_idx, &bytes_to_sign)?
155
            {
156
                return Err(TxValidationError::ReducedToFalse(input_idx, res));
1✔
157
            }
158
        }
159
        Ok(())
1✔
160
    }
161
}
162

163
fn verify_output(
1✔
164
    state_context: &ErgoStateContext,
165
    output: &ErgoBox,
166
    max_creation_height: u32,
167
) -> Result<(), TxValidationError> {
168
    let box_size = output.sigma_serialize_bytes()?.len() as u64;
1✔
169
    let script_size = output.script_bytes()?.len();
1✔
170
    let block_version = state_context.pre_header.version;
1✔
171
    // Check that output is not dust
172
    let minimum_value = box_size * state_context.parameters.min_value_per_byte() as u64;
1✔
173
    if *output.value.as_u64() < minimum_value {
1✔
NEW
174
        return Err(TxValidationError::DustOutput(
×
NEW
175
            output.box_id(),
×
NEW
176
            output.value,
×
177
            minimum_value,
178
        ));
179
    }
180
    // Check that height does not exceed maximum height. Note that heights can be potentially negative in V1
181
    if output.creation_height as i32 > state_context.pre_header.height as i32 {
1✔
NEW
182
        return Err(TxValidationError::InvalidHeightError(
×
NEW
183
            output.creation_height,
×
184
        ));
185
    }
186
    if output.creation_height < max_creation_height {
1✔
187
        return Err(TxValidationError::MonotonicHeightError(
1✔
188
            output.creation_height,
1✔
189
            max_creation_height,
190
        ));
191
    }
192
    // Negative output heights were allowed in V1. sigma-rust always stores heights as unsigned integers
193
    if block_version != 1 && output.creation_height & (1 << 31) != 0 {
2✔
NEW
194
        return Err(TxValidationError::NegativeHeight);
×
195
    }
196
    if box_size as usize > ErgoBox::MAX_BOX_SIZE {
1✔
NEW
197
        return Err(TxValidationError::BoxSizeExceeded(box_size as usize));
×
198
    }
199
    if script_size > ErgoBox::MAX_SCRIPT_SIZE {
1✔
NEW
200
        return Err(TxValidationError::ScriptSizeExceeded(script_size));
×
201
    }
202
    Ok(())
1✔
203
}
204

205
// Extract all of the assets in a collection of boxes for transaction validation
206
fn extract_assets<'a, I: Iterator<Item = &'a Option<BoxTokens>>>(
2✔
207
    mut boxes: I,
208
) -> Result<HashMap<TokenId, TokenAmount>, TxValidationError> {
209
    boxes.try_fold(
2✔
210
        HashMap::new(),
2✔
211
        |mut asset_map: HashMap<TokenId, TokenAmount>, tokens| {
2✔
212
            tokens
6✔
NEW
213
                .as_ref()
×
NEW
214
                .into_iter()
×
NEW
215
                .flatten()
×
216
                .try_for_each(|token| {
4✔
217
                    match asset_map.entry(token.token_id) {
2✔
218
                        Entry::Occupied(mut occ) => {
1✔
219
                            *occ.get_mut() = occ.get().checked_add(&token.amount)?;
2✔
220
                        }
221
                        Entry::Vacant(vac) => {
2✔
222
                            vac.insert(token.amount);
2✔
223
                        }
224
                    }
225
                    Ok::<(), TxValidationError>(())
2✔
226
                })?;
227
            Ok(asset_map)
2✔
228
        },
229
    )
230
}
231

232
fn verify_assets(
1✔
233
    inputs: &[BoxId],
234
    in_assets: HashMap<TokenId, TokenAmount>,
235
    out_assets: HashMap<TokenId, TokenAmount>,
236
) -> Result<(), TxValidationError> {
237
    // If this transaction mints a new token, it's token ID must be the ID of the first box being spent
238
    let new_token_id: TokenId = inputs[0].into();
1✔
239
    for (&out_token_id, &out_amount) in &out_assets {
2✔
240
        if let Some(&in_amount) = in_assets.get(&out_token_id) {
2✔
241
            // Check that Transaction is not creating tokens out of thin air
242
            if in_amount < out_amount {
2✔
243
                return Err(TxValidationError::TokenPreservationError {
1✔
244
                    token_id: out_token_id,
1✔
245
                    in_amount: in_amount.into(),
1✔
246
                    out_amount: out_amount.into(),
1✔
247
                    new_token_id,
1✔
248
                });
249
            }
250
        } else if out_token_id != new_token_id {
2✔
251
            //minting a new token. Token amount checks are handled by the TokenAmount newtype and not needed here
NEW
252
            return Err(TxValidationError::TokenPreservationError {
×
NEW
253
                token_id: out_token_id,
×
254
                in_amount: 0,
NEW
255
                out_amount: out_amount.into(),
×
NEW
256
                new_token_id,
×
257
            });
258
        }
259
    }
260
    Ok(())
1✔
261
}
262

263
/// Transaction context errors
264
#[derive(Error, Debug)]
265
pub 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)]
288
mod 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc