• 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

70.4
/ergo-lib/src/chain/transaction.rs
1
//! Ergo transaction
2

3
mod data_input;
4
pub mod ergo_transaction;
5
pub mod input;
6
pub mod reduced;
7
pub(crate) mod storage_rent;
8
pub mod unsigned;
9

10
use bounded_vec::BoundedVec;
11
use ergo_chain_types::blake2b256_hash;
12
pub use ergotree_interpreter::eval::context::TxIoVec;
13
use ergotree_interpreter::eval::env::Env;
14
use ergotree_interpreter::eval::extract_sigma_boolean;
15
use ergotree_interpreter::eval::EvalError;
16
use ergotree_interpreter::eval::ReductionDiagnosticInfo;
17
use ergotree_interpreter::sigma_protocol::verifier::verify_signature;
18
use ergotree_interpreter::sigma_protocol::verifier::TestVerifier;
19
use ergotree_interpreter::sigma_protocol::verifier::VerificationResult;
20
use ergotree_interpreter::sigma_protocol::verifier::Verifier;
21
use ergotree_interpreter::sigma_protocol::verifier::VerifierError;
22
use ergotree_ir::chain::ergo_box::BoxId;
23
use ergotree_ir::chain::ergo_box::ErgoBox;
24
use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
25
use ergotree_ir::chain::token::TokenId;
26
pub use ergotree_ir::chain::tx_id::TxId;
27
use ergotree_ir::ergo_tree::ErgoTreeError;
28
use thiserror::Error;
29

30
pub use data_input::*;
31
use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
32
use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
33
use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
34
use ergotree_ir::serialization::SigmaParsingError;
35
use ergotree_ir::serialization::SigmaSerializable;
36
use ergotree_ir::serialization::SigmaSerializationError;
37
use ergotree_ir::serialization::SigmaSerializeResult;
38
pub use input::*;
39

40
use crate::wallet::signing::make_context;
41
use crate::wallet::signing::TransactionContext;
42
use crate::wallet::tx_context::TransactionContextError;
43

44
use self::ergo_transaction::TxValidationError;
45
use self::storage_rent::try_spend_storage_rent;
46
use self::unsigned::UnsignedTransaction;
47

48
use indexmap::IndexSet;
49

50
use std::convert::TryFrom;
51
use std::convert::TryInto;
52
use std::iter::FromIterator;
53
use std::rc::Rc;
54

55
use super::ergo_state_context::ErgoStateContext;
56

57
/**
58
 * ErgoTransaction is an atomic state transition operation. It destroys Boxes from the state
59
 * and creates new ones. If transaction is spending boxes protected by some non-trivial scripts,
60
 * its inputs should also contain proof of spending correctness - context extension (user-defined
61
 * key-value map) and data inputs (links to existing boxes in the state) that may be used during
62
 * script reduction to crypto, signatures that satisfies the remaining cryptographic protection
63
 * of the script.
64
 * Transactions are not encrypted, so it is possible to browse and view every transaction ever
65
 * collected into a block.
66
 */
67
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
68
#[cfg_attr(
69
    feature = "json",
70
    serde(
71
        try_from = "super::json::transaction::TransactionJson",
72
        into = "super::json::transaction::TransactionJson"
73
    )
74
)]
75
#[derive(PartialEq, Eq, Debug, Clone)]
76
pub struct Transaction {
77
    /// transaction id
78
    pub(crate) tx_id: TxId,
79
    /// inputs, that will be spent by this transaction.
80
    pub inputs: TxIoVec<Input>,
81
    /// inputs, that are not going to be spent by transaction, but will be reachable from inputs
82
    /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not
83
    /// included in transaction cost and they do not contain spending proofs.
84
    pub data_inputs: Option<TxIoVec<DataInput>>,
85

86
    /// box candidates to be created by this transaction. Differ from [`Self::outputs`] in that
87
    /// they do not include transaction id and index
88
    pub output_candidates: TxIoVec<ErgoBoxCandidate>,
89

90
    /// Boxes to be created by this transaction. Differ from [`Self::output_candidates`] in that
91
    /// they include transaction id and index
92
    pub outputs: TxIoVec<ErgoBox>,
93
}
94

95
impl Transaction {
96
    /// Maximum number of outputs
97
    pub const MAX_OUTPUTS_COUNT: usize = u16::MAX as usize;
98

99
    /// Creates new transaction from vectors
100
    pub fn new_from_vec(
1✔
101
        inputs: Vec<Input>,
102
        data_inputs: Vec<DataInput>,
103
        output_candidates: Vec<ErgoBoxCandidate>,
104
    ) -> Result<Transaction, TransactionError> {
105
        Ok(Transaction::new(
4✔
106
            inputs
2✔
107
                .try_into()
108
                .map_err(TransactionError::InvalidInputsCount)?,
×
109
            BoundedVec::opt_empty_vec(data_inputs)
2✔
110
                .map_err(TransactionError::InvalidDataInputsCount)?,
×
111
            output_candidates
2✔
112
                .try_into()
113
                .map_err(TransactionError::InvalidOutputCandidatesCount)?,
×
114
        )?)
115
    }
116

117
    /// Creates new transaction
118
    pub fn new(
5✔
119
        inputs: TxIoVec<Input>,
120
        data_inputs: Option<TxIoVec<DataInput>>,
121
        output_candidates: TxIoVec<ErgoBoxCandidate>,
122
    ) -> Result<Transaction, SigmaSerializationError> {
123
        let outputs_with_zero_tx_id =
7✔
124
            output_candidates
125
                .clone()
126
                .enumerated()
127
                .try_mapped_ref(|(idx, bc)| {
10✔
128
                    ErgoBox::from_box_candidate(bc, TxId::zero(), *idx as u16)
5✔
129
                })?;
130
        let tx_to_sign = Transaction {
131
            tx_id: TxId::zero(),
5✔
132
            inputs,
133
            data_inputs,
134
            output_candidates: output_candidates.clone(),
5✔
135
            outputs: outputs_with_zero_tx_id,
136
        };
137
        let tx_id = tx_to_sign.calc_tx_id()?;
11✔
138
        let outputs = output_candidates
18✔
139
            .enumerated()
140
            .try_mapped_ref(|(idx, bc)| ErgoBox::from_box_candidate(bc, tx_id, *idx as u16))?;
24✔
141
        Ok(Transaction {
6✔
142
            tx_id,
6✔
143
            outputs,
6✔
144
            ..tx_to_sign
145
        })
146
    }
147

148
    /// Create Transaction from UnsignedTransaction and an array of proofs in the same order as
149
    /// UnsignedTransaction.inputs
150
    pub fn from_unsigned_tx(
×
151
        unsigned_tx: UnsignedTransaction,
152
        proofs: Vec<ProofBytes>,
153
    ) -> Result<Self, TransactionError> {
154
        let inputs = unsigned_tx
×
155
            .inputs
156
            .enumerated()
157
            .try_mapped(|(index, unsigned_input)| {
×
158
                proofs
×
159
                    .get(index)
×
160
                    .map(|proof| Input::from_unsigned_input(unsigned_input, proof.clone()))
×
161
                    .ok_or_else(|| {
×
162
                        TransactionError::InvalidArgument(format!(
×
163
                            "no proof for input index: {}",
164
                            index
165
                        ))
166
                    })
167
            })?;
168
        Ok(Transaction::new(
×
169
            inputs,
×
170
            unsigned_tx.data_inputs,
×
171
            unsigned_tx.output_candidates,
×
172
        )?)
173
    }
174

175
    fn calc_tx_id(&self) -> Result<TxId, SigmaSerializationError> {
5✔
176
        let bytes = self.bytes_to_sign()?;
5✔
177
        Ok(TxId(blake2b256_hash(&bytes)))
12✔
178
    }
179

180
    /// Serialized tx with empty proofs
181
    pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
5✔
182
        let empty_proof_inputs = self.inputs.mapped_ref(|i| i.input_to_sign());
15✔
183
        let tx_to_sign = Transaction {
184
            inputs: empty_proof_inputs,
185
            ..(*self).clone()
186
        };
187
        tx_to_sign.sigma_serialize_bytes()
5✔
188
    }
189

190
    /// Get transaction id
191
    pub fn id(&self) -> TxId {
1✔
192
        self.tx_id
1✔
193
    }
194

195
    /// Check the signature of the transaction's input corresponding
196
    /// to the given input box, guarded by P2PK script
197
    pub fn verify_p2pk_input(
×
198
        &self,
199
        input_box: ErgoBox,
200
    ) -> Result<bool, TransactionSignatureVerificationError> {
201
        #[allow(clippy::unwrap_used)]
202
        // since we have a tx with tx_id at this point, serialization is safe to unwrap
203
        let message = self.bytes_to_sign().unwrap();
×
204
        let input = self
×
205
            .inputs
206
            .iter()
207
            .find(|input| input.box_id == input_box.box_id())
×
208
            .ok_or_else(|| {
×
209
                TransactionSignatureVerificationError::InputNotFound(input_box.box_id())
×
210
            })?;
211
        let sb = extract_sigma_boolean(&input_box.ergo_tree.proposition()?)?;
×
212
        Ok(verify_signature(
×
213
            sb,
×
214
            message.as_slice(),
×
215
            input.spending_proof.proof.as_ref(),
×
216
        )?)
217
    }
218
}
219

220
#[allow(missing_docs)]
221
#[derive(Error, Debug)]
222
pub enum TransactionSignatureVerificationError {
223
    #[error("Input with id {0:?} not found")]
224
    InputNotFound(BoxId),
225
    #[error("input signature verification failed: {0:?}")]
226
    VerifierError(#[from] VerifierError),
227
    #[error("ErgoTreeError: {0}")]
228
    ErgoTreeError(#[from] ErgoTreeError),
229
    #[error("EvalError: {0}")]
230
    EvalError(#[from] EvalError),
231
}
232

233
/// Returns distinct token ids from all given ErgoBoxCandidate's
234
pub fn distinct_token_ids<I>(output_candidates: I) -> IndexSet<TokenId>
5✔
235
where
236
    I: IntoIterator<Item = ErgoBoxCandidate>,
237
{
238
    let token_ids: Vec<TokenId> = output_candidates
5✔
239
        .into_iter()
240
        .flat_map(|b| {
5✔
241
            b.tokens
10✔
242
                .into_iter()
×
243
                .flatten()
×
244
                .map(|t| t.token_id)
2✔
245
                .collect::<Vec<TokenId>>()
×
246
        })
247
        .collect();
248
    IndexSet::<_>::from_iter(token_ids)
5✔
249
}
250

251
impl SigmaSerializable for Transaction {
252
    #[allow(clippy::unwrap_used)]
253
    fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
5✔
254
        // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala#L112-L112
255
        w.put_usize_as_u16_unwrapped(self.inputs.len())?;
5✔
256
        self.inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
15✔
257
        if let Some(data_inputs) = &self.data_inputs {
6✔
258
            w.put_usize_as_u16_unwrapped(data_inputs.len())?;
1✔
259
            data_inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
3✔
260
        } else {
261
            w.put_u16(0)?;
5✔
262
        }
263

264
        // Serialize distinct ids of tokens in transaction outputs.
265
        let distinct_token_ids = distinct_token_ids(self.output_candidates.clone());
5✔
266

267
        // Note that `self.output_candidates` is of type `TxIoVec` which has a max length of
268
        // `u16::MAX`. Therefore the following unwrap is safe.
269
        w.put_u32(u32::try_from(distinct_token_ids.len()).unwrap())?;
10✔
270
        distinct_token_ids
15✔
271
            .iter()
272
            .try_for_each(|t_id| t_id.sigma_serialize(w))?;
7✔
273

274
        // serialize outputs
275
        w.put_usize_as_u16_unwrapped(self.output_candidates.len())?;
10✔
276
        self.output_candidates.iter().try_for_each(|o| {
20✔
277
            ErgoBoxCandidate::serialize_body_with_indexed_digests(o, Some(&distinct_token_ids), w)
5✔
278
        })?;
279
        Ok(())
5✔
280
    }
281

282
    fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
2✔
283
        // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala#L146-L146
284

285
        // parse transaction inputs
286
        let inputs_count = r.get_u16()?;
2✔
287
        let mut inputs = Vec::with_capacity(inputs_count as usize);
2✔
288
        for _ in 0..inputs_count {
4✔
289
            inputs.push(Input::sigma_parse(r)?);
4✔
290
        }
291

292
        // parse transaction data inputs
293
        let data_inputs_count = r.get_u16()?;
4✔
294
        let mut data_inputs = Vec::with_capacity(data_inputs_count as usize);
4✔
295
        for _ in 0..data_inputs_count {
4✔
296
            data_inputs.push(DataInput::sigma_parse(r)?);
4✔
297
        }
298

299
        // parse distinct ids of tokens in transaction outputs
300
        let tokens_count = r.get_u32()?;
4✔
301
        if tokens_count as usize > Transaction::MAX_OUTPUTS_COUNT * ErgoBox::MAX_TOKENS_COUNT {
4✔
302
            return Err(SigmaParsingError::ValueOutOfBounds(
×
303
                "too many tokens in transaction".to_string(),
×
304
            ));
305
        }
306
        let mut token_ids = IndexSet::with_capacity(tokens_count as usize);
2✔
307
        for _ in 0..tokens_count {
4✔
308
            token_ids.insert(TokenId::sigma_parse(r)?);
4✔
309
        }
310

311
        // parse outputs
312
        let outputs_count = r.get_u16()?;
4✔
313
        let mut outputs = Vec::with_capacity(outputs_count as usize);
4✔
314
        for _ in 0..outputs_count {
4✔
315
            outputs.push(ErgoBoxCandidate::parse_body_with_indexed_digests(
4✔
316
                Some(&token_ids),
2✔
317
                r,
×
318
            )?)
319
        }
320

321
        Transaction::new_from_vec(inputs, data_inputs, outputs)
4✔
322
            .map_err(|e| SigmaParsingError::Misc(format!("{}", e)))
×
323
    }
324
}
325

326
/// Error when working with Transaction
327
#[allow(missing_docs)]
328
#[derive(Error, Eq, PartialEq, Debug, Clone)]
329
pub enum TransactionError {
330
    #[error("Tx serialization error: {0}")]
331
    SigmaSerializationError(#[from] SigmaSerializationError),
332
    #[error("Tx innvalid argument: {0}")]
333
    InvalidArgument(String),
334
    #[error("Invalid Tx inputs: {0:?}")]
335
    InvalidInputsCount(bounded_vec::BoundedVecOutOfBounds),
336
    #[error("Invalid Tx output_candidates: {0:?}")]
337
    InvalidOutputCandidatesCount(bounded_vec::BoundedVecOutOfBounds),
338
    #[error("Invalid Tx data inputs: {0:?}")]
339
    InvalidDataInputsCount(bounded_vec::BoundedVecOutOfBounds),
340
    #[error("input with index {0} not found")]
341
    InputNofFound(usize),
342
}
343

344
/// Verify transaction input's proof
345
pub fn verify_tx_input_proof(
1✔
346
    tx_context: &TransactionContext<Transaction>,
347
    state_context: &ErgoStateContext,
348
    input_idx: usize,
349
    bytes_to_sign: &[u8],
350
) -> Result<VerificationResult, TxValidationError> {
351
    let input = tx_context
3✔
352
        .spending_tx
353
        .inputs
354
        .get(input_idx)
1✔
355
        .ok_or(TransactionContextError::InputBoxNotFound(input_idx))?;
1✔
356
    let input_box = tx_context
3✔
357
        .get_input_box(&input.box_id)
1✔
358
        .ok_or(TransactionContextError::InputBoxNotFound(input_idx))?;
1✔
359
    let ctx = Rc::new(make_context(state_context, tx_context, input_idx)?);
2✔
360
    let verifier = TestVerifier;
1✔
361
    // Try spending in storage rent, if any condition is not satisfied fallback to normal script validation
362
    match try_spend_storage_rent(input, state_context, &ctx) {
1✔
NEW
363
        Some(()) => Ok(VerificationResult {
×
364
            result: true,
365
            cost: 0,
NEW
366
            diag: ReductionDiagnosticInfo {
×
NEW
367
                env: Env::empty(),
×
NEW
368
                pretty_printed_expr: None,
×
369
            },
370
        }),
371
        None => verifier
2✔
372
            .verify(
373
                &input_box.ergo_tree,
1✔
374
                &Env::empty(),
1✔
375
                ctx,
1✔
376
                input.spending_proof.proof.clone(),
1✔
377
                bytes_to_sign,
378
            )
379
            .map_err(|e| TxValidationError::VerifierError(input_idx, e)),
1✔
380
    }
381
}
382

383
/// Arbitrary impl
384
#[cfg(feature = "arbitrary")]
385
#[allow(clippy::unwrap_used)]
386
pub mod arbitrary {
387

388
    use super::*;
389
    use proptest::prelude::*;
390
    use proptest::{arbitrary::Arbitrary, collection::vec};
391

392
    impl Arbitrary for Transaction {
393
        type Parameters = ();
394

395
        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1✔
396
            (
397
                vec(any::<Input>(), 1..10),
1✔
398
                vec(any::<DataInput>(), 0..10),
2✔
399
                vec(any::<ErgoBoxCandidate>(), 1..10),
2✔
400
            )
401
                .prop_map(|(inputs, data_inputs, outputs)| {
2✔
402
                    Self::new_from_vec(inputs, data_inputs, outputs).unwrap()
1✔
403
                })
404
                .boxed()
405
        }
406
        type Strategy = BoxedStrategy<Self>;
407
    }
408
}
409

410
#[cfg(test)]
411
#[allow(clippy::unwrap_used, clippy::panic)]
412
pub mod tests {
413

414
    use super::*;
415

416
    use ergotree_ir::serialization::sigma_serialize_roundtrip;
417
    use proptest::prelude::*;
418

419
    proptest! {
420

421
        #![proptest_config(ProptestConfig::with_cases(64))]
422

423
        #[test]
424
        fn tx_ser_roundtrip(v in any::<Transaction>()) {
425
            prop_assert_eq![sigma_serialize_roundtrip(&v), v];
426
        }
427

428

429
        #[test]
430
        fn tx_id_ser_roundtrip(v in any::<TxId>()) {
431
            prop_assert_eq![sigma_serialize_roundtrip(&v), v];
432
        }
433

434
    }
435

436
    #[test]
437
    #[cfg(feature = "json")]
438
    fn test_tx_id_calc() {
439
        let json = r#"
440
        {
441
      "id": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
442
      "inputs": [
443
        {
444
          "boxId": "9126af0675056b80d1fda7af9bf658464dbfa0b128afca7bf7dae18c27fe8456",
445
          "spendingProof": {
446
            "proofBytes": "",
447
            "extension": {}
448
          }
449
        }
450
      ],
451
      "dataInputs": [],
452
      "outputs": [
453
        {
454
          "boxId": "b979c439dc698ce5e823b21c722a6e23721af010e4df8c72de0bfd0c3d9ccf6b",
455
          "value": 74187765000000000,
456
          "ergoTree": "101004020e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a7017300730110010204020404040004c0fd4f05808c82f5f6030580b8c9e5ae040580f882ad16040204c0944004c0f407040004000580f882ad16d19683030191a38cc7a7019683020193c2b2a57300007473017302830108cdeeac93a38cc7b2a573030001978302019683040193b1a5730493c2a7c2b2a573050093958fa3730673079973089c73097e9a730a9d99a3730b730c0599c1a7c1b2a5730d00938cc7b2a5730e0001a390c1a7730f",
457
          "assets": [],
458
          "creationHeight": 284761,
459
          "additionalRegisters": {},
460
          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
461
          "index": 0
462
        },
463
        {
464
          "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
465
          "value": 67500000000,
466
          "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
467
          "assets": [],
468
          "creationHeight": 284761,
469
          "additionalRegisters": {},
470
          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
471
          "index": 1
472
        }
473
      ]
474
    }"#;
475
        let res = serde_json::from_str(json);
476
        let t: Transaction = res.unwrap();
477
        let tx_id_str: String = t.id().into();
478
        assert_eq!(
479
            "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
480
            tx_id_str
481
        )
482
    }
483
}
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