1mod 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#[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 pub(crate) tx_id: TxId,
79 pub inputs: TxIoVec<Input>,
81 pub data_inputs: Option<TxIoVec<DataInput>>,
85
86 pub output_candidates: TxIoVec<ErgoBoxCandidate>,
89
90 pub outputs: TxIoVec<ErgoBox>,
93}
94
95impl Transaction {
96 pub const MAX_OUTPUTS_COUNT: usize = u16::MAX as usize;
98
99 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 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 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 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 pub fn id(&self) -> TxId {
192 self.tx_id
193 }
194
195 pub fn verify_p2pk_input(
198 &self,
199 input_box: ErgoBox,
200 ) -> Result<bool, TransactionSignatureVerificationError> {
201 #[allow(clippy::unwrap_used)]
202 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
233pub 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 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 let distinct_token_ids = distinct_token_ids(self.output_candidates.clone());
266
267 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 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 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 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 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 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#[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
344pub 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 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#[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}