ergo_lib/chain/
transaction.rs

1//! Ergo transaction
2
3mod data_input;
4pub mod ergo_transaction;
5pub mod input;
6pub mod reduced;
7pub(crate) mod storage_rent;
8pub mod unsigned;
9
10use bounded_vec::BoundedVec;
11use ergo_chain_types::blake2b256_hash;
12use ergotree_interpreter::eval::context::Context;
13pub use ergotree_interpreter::eval::context::TxIoVec;
14use ergotree_interpreter::eval::env::Env;
15use ergotree_interpreter::eval::extract_sigma_boolean;
16use ergotree_interpreter::eval::EvalError;
17use ergotree_interpreter::eval::ReductionDiagnosticInfo;
18use ergotree_interpreter::sigma_protocol::verifier::verify_signature;
19use ergotree_interpreter::sigma_protocol::verifier::TestVerifier;
20use ergotree_interpreter::sigma_protocol::verifier::VerificationResult;
21use ergotree_interpreter::sigma_protocol::verifier::Verifier;
22use ergotree_interpreter::sigma_protocol::verifier::VerifierError;
23use ergotree_ir::chain::ergo_box::BoxId;
24use ergotree_ir::chain::ergo_box::ErgoBox;
25use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
26use ergotree_ir::chain::token::TokenId;
27pub use ergotree_ir::chain::tx_id::TxId;
28use ergotree_ir::ergo_tree::ErgoTreeError;
29use thiserror::Error;
30
31pub use data_input::*;
32use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
33use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
34use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
35use ergotree_ir::serialization::SigmaParsingError;
36use ergotree_ir::serialization::SigmaSerializable;
37use ergotree_ir::serialization::SigmaSerializationError;
38use ergotree_ir::serialization::SigmaSerializeResult;
39pub use input::*;
40
41use crate::wallet::signing::update_context;
42use crate::wallet::signing::TransactionContext;
43use crate::wallet::tx_context::TransactionContextError;
44
45use self::ergo_transaction::TxValidationError;
46use self::storage_rent::try_spend_storage_rent;
47use self::unsigned::UnsignedTransaction;
48
49use indexmap::IndexSet;
50
51use std::convert::TryFrom;
52use std::convert::TryInto;
53use std::iter::FromIterator;
54
55use 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)]
76pub 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
95impl 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(
101        inputs: Vec<Input>,
102        data_inputs: Vec<DataInput>,
103        output_candidates: Vec<ErgoBoxCandidate>,
104    ) -> Result<Transaction, TransactionError> {
105        Ok(Transaction::new(
106            inputs
107                .try_into()
108                .map_err(TransactionError::InvalidInputsCount)?,
109            BoundedVec::opt_empty_vec(data_inputs)
110                .map_err(TransactionError::InvalidDataInputsCount)?,
111            output_candidates
112                .try_into()
113                .map_err(TransactionError::InvalidOutputCandidatesCount)?,
114        )?)
115    }
116
117    /// Creates new transaction
118    pub fn new(
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 =
124            output_candidates
125                .clone()
126                .enumerated()
127                .try_mapped_ref(|(idx, bc)| {
128                    ErgoBox::from_box_candidate(bc, TxId::zero(), *idx as u16)
129                })?;
130        let tx_to_sign = Transaction {
131            tx_id: TxId::zero(),
132            inputs,
133            data_inputs,
134            output_candidates: output_candidates.clone(),
135            outputs: outputs_with_zero_tx_id,
136        };
137        let tx_id = tx_to_sign.calc_tx_id()?;
138        let outputs = output_candidates
139            .enumerated()
140            .try_mapped_ref(|(idx, bc)| ErgoBox::from_box_candidate(bc, tx_id, *idx as u16))?;
141        Ok(Transaction {
142            tx_id,
143            outputs,
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> {
176        let bytes = self.bytes_to_sign()?;
177        Ok(TxId(blake2b256_hash(&bytes)))
178    }
179
180    /// Serialized tx with empty proofs
181    pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
182        let empty_proof_inputs = self.inputs.mapped_ref(|i| i.input_to_sign());
183        let tx_to_sign = Transaction {
184            inputs: empty_proof_inputs,
185            ..(*self).clone()
186        };
187        tx_to_sign.sigma_serialize_bytes()
188    }
189
190    /// Get transaction id
191    pub fn id(&self) -> TxId {
192        self.tx_id
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)]
222pub 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
234pub fn distinct_token_ids<I>(output_candidates: I) -> IndexSet<TokenId>
235where
236    I: IntoIterator<Item = ErgoBoxCandidate>,
237{
238    let token_ids: Vec<TokenId> = output_candidates
239        .into_iter()
240        .flat_map(|b| {
241            b.tokens
242                .into_iter()
243                .flatten()
244                .map(|t| t.token_id)
245                .collect::<Vec<TokenId>>()
246        })
247        .collect();
248    IndexSet::<_>::from_iter(token_ids)
249}
250
251impl SigmaSerializable for Transaction {
252    #[allow(clippy::unwrap_used)]
253    fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
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())?;
256        self.inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
257        if let Some(data_inputs) = &self.data_inputs {
258            w.put_usize_as_u16_unwrapped(data_inputs.len())?;
259            data_inputs.iter().try_for_each(|i| i.sigma_serialize(w))?;
260        } else {
261            w.put_u16(0)?;
262        }
263
264        // Serialize distinct ids of tokens in transaction outputs.
265        let distinct_token_ids = distinct_token_ids(self.output_candidates.clone());
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())?;
270        distinct_token_ids
271            .iter()
272            .try_for_each(|t_id| t_id.sigma_serialize(w))?;
273
274        // serialize outputs
275        w.put_usize_as_u16_unwrapped(self.output_candidates.len())?;
276        self.output_candidates.iter().try_for_each(|o| {
277            ErgoBoxCandidate::serialize_body_with_indexed_digests(o, Some(&distinct_token_ids), w)
278        })?;
279        Ok(())
280    }
281
282    fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
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()?;
287        let mut inputs = Vec::with_capacity(inputs_count as usize);
288        for _ in 0..inputs_count {
289            inputs.push(Input::sigma_parse(r)?);
290        }
291
292        // parse transaction data inputs
293        let data_inputs_count = r.get_u16()?;
294        let mut data_inputs = Vec::with_capacity(data_inputs_count as usize);
295        for _ in 0..data_inputs_count {
296            data_inputs.push(DataInput::sigma_parse(r)?);
297        }
298
299        // parse distinct ids of tokens in transaction outputs
300        let tokens_count = r.get_u32()?;
301        if tokens_count as usize > Transaction::MAX_OUTPUTS_COUNT * ErgoBox::MAX_TOKENS_COUNT {
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);
307        for _ in 0..tokens_count {
308            token_ids.insert(TokenId::sigma_parse(r)?);
309        }
310
311        // parse outputs
312        let outputs_count = r.get_u16()?;
313        let mut outputs = Vec::with_capacity(outputs_count as usize);
314        for _ in 0..outputs_count {
315            outputs.push(ErgoBoxCandidate::parse_body_with_indexed_digests(
316                Some(&token_ids),
317                r,
318            )?)
319        }
320
321        Transaction::new_from_vec(inputs, data_inputs, outputs)
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)]
329pub 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
345pub fn verify_tx_input_proof<'ctx>(
346    tx_context: &'ctx TransactionContext<Transaction>,
347    ctx: &mut Context<'ctx>,
348    state_context: &ErgoStateContext,
349    input_idx: usize,
350    bytes_to_sign: &[u8],
351) -> Result<VerificationResult, TxValidationError> {
352    update_context(ctx, tx_context, input_idx)?;
353    let input = tx_context
354        .spending_tx
355        .inputs
356        .get(input_idx)
357        .ok_or(TransactionContextError::InputBoxNotFound(input_idx))?;
358    let input_box = tx_context
359        .get_input_box(&input.box_id)
360        .ok_or(TransactionContextError::InputBoxNotFound(input_idx))?;
361    let verifier = TestVerifier;
362    // Try spending in storage rent, if any condition is not satisfied fallback to normal script validation
363    match try_spend_storage_rent(input, input_box, state_context, ctx) {
364        Some(()) => Ok(VerificationResult {
365            result: true,
366            cost: 0,
367            diag: ReductionDiagnosticInfo {
368                env: Env::empty(),
369                pretty_printed_expr: None,
370            },
371        }),
372        None => verifier
373            .verify(
374                &input_box.ergo_tree,
375                ctx,
376                input.spending_proof.proof.clone(),
377                bytes_to_sign,
378            )
379            .map_err(|e| TxValidationError::VerifierError(input_idx, e)),
380    }
381}
382
383/// Arbitrary impl
384#[cfg(feature = "arbitrary")]
385#[allow(clippy::unwrap_used)]
386pub 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 {
396            (
397                vec(any::<Input>(), 1..10),
398                vec(any::<DataInput>(), 0..10),
399                vec(any::<ErgoBoxCandidate>(), 1..10),
400            )
401                .prop_map(|(inputs, data_inputs, outputs)| {
402                    Self::new_from_vec(inputs, data_inputs, outputs).unwrap()
403                })
404                .boxed()
405        }
406        type Strategy = BoxedStrategy<Self>;
407    }
408}
409
410#[cfg(test)]
411#[allow(clippy::unwrap_used, clippy::panic)]
412pub 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}