dubp_documents/transaction/
v10.rs

1//  Copyright (C) 2020  Éloïs SANCHEZ.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as
5// published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Wrappers around Transaction documents.
17
18mod builder;
19mod input_proof_output;
20mod stringified;
21mod unsigned;
22
23pub use builder::TransactionDocumentV10Builder;
24pub use input_proof_output::{
25    TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10,
26};
27pub use stringified::TransactionDocumentV10Stringified;
28pub use unsigned::UnsignedTransactionDocumentV10;
29
30use crate::*;
31
32const TX_V10_MAX_LINES: usize = 100;
33
34/// Wrap a Transaction document.
35///
36/// Must be created by parsing a text document or using a builder.
37#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
38pub struct TransactionDocumentV10 {
39    /// Document as text.
40    ///
41    /// Is used to check signatures, and other values
42    /// must be extracted from it.
43    text: Option<String>,
44
45    /// Currency.
46    currency: String,
47    /// Blockstamp
48    blockstamp: Blockstamp,
49    /// Locktime
50    locktime: u64,
51    /// Document issuers.
52    issuers: SmallVec<[ed25519::PublicKey; 1]>,
53    /// Transaction inputs.
54    inputs: Vec<TransactionInputV10>,
55    /// Inputs unlocks.
56    unlocks: Vec<TransactionInputUnlocksV10>,
57    /// Transaction outputs.
58    outputs: SmallVec<[TransactionOutputV10; 2]>,
59    /// Transaction comment
60    comment: String,
61    /// Document signatures.
62    signatures: SmallVec<[ed25519::Signature; 1]>,
63    /// Transaction hash
64    hash: Option<Hash>,
65}
66
67impl<'a> TransactionDocumentTrait<'a> for TransactionDocumentV10 {
68    type Address = WalletScriptV10;
69    type Input = TransactionInputV10;
70    type Inputs = &'a [TransactionInputV10];
71    type InputUnlocks = TransactionInputUnlocksV10;
72    type InputsUnlocks = &'a [TransactionInputUnlocksV10];
73    type Output = TransactionOutputV10;
74    type Outputs = &'a [TransactionOutputV10];
75    type PubKey = ed25519::PublicKey;
76    type UnsignedDoc = UnsignedTransactionDocumentV10;
77
78    fn generate_simple_txs(
79        blockstamp: Blockstamp,
80        currency: String,
81        inputs_with_sum: (Vec<Self::Input>, SourceAmount),
82        issuer: Self::PubKey,
83        recipient: WalletScriptV10,
84        user_amount_and_comment: (SourceAmount, String),
85        cash_back_pubkey: Option<Self::PubKey>,
86    ) -> Vec<Self::UnsignedDoc> {
87        let (inputs, inputs_sum) = inputs_with_sum;
88        let (user_amount, user_comment) = user_amount_and_comment;
89        super::v10_gen::TransactionDocV10SimpleGen {
90            blockstamp,
91            currency,
92            inputs,
93            inputs_sum,
94            issuer,
95            recipient,
96            user_amount,
97            user_comment,
98            cash_back_pubkey,
99        }
100        .gen()
101    }
102    fn get_inputs(&'a self) -> Self::Inputs {
103        &self.inputs
104    }
105    fn get_inputs_unlocks(&'a self) -> Self::InputsUnlocks {
106        &self.unlocks
107    }
108    fn get_outputs(&'a self) -> Self::Outputs {
109        &self.outputs
110    }
111    fn verify(&self, expected_currency_opt: Option<&str>) -> Result<(), super::TxVerifyErr> {
112        if let Some(expected_currency) = expected_currency_opt {
113            if self.currency != expected_currency {
114                return Err(super::TxVerifyErr::WrongCurrency {
115                    expected: expected_currency.to_owned(),
116                    found: self.currency.clone(),
117                });
118            }
119        }
120        if self.inputs.is_empty() {
121            return Err(super::TxVerifyErr::NoInput);
122        }
123        if self.issuers.is_empty() {
124            return Err(super::TxVerifyErr::NoIssuer);
125        }
126        if self.outputs.is_empty() {
127            return Err(super::TxVerifyErr::NoOutput);
128        }
129        if self.inputs.len() != self.unlocks.len() {
130            return Err(super::TxVerifyErr::NotSameAmountOfInputsAndUnlocks(
131                self.inputs.len(),
132                self.unlocks.len(),
133            ));
134        }
135        self.verify_signatures().map_err(super::TxVerifyErr::Sigs)?;
136        let lines_count = self.compact_size_in_lines();
137        if lines_count > TX_V10_MAX_LINES {
138            return Err(super::TxVerifyErr::TooManyLines {
139                found: lines_count,
140                max: TX_V10_MAX_LINES,
141            });
142        }
143        let inputs_amount: SourceAmount = self.inputs.iter().map(|input| input.amount).sum();
144        let outputs_amount: SourceAmount = self.outputs.iter().map(|output| output.amount).sum();
145        if inputs_amount != outputs_amount {
146            return Err(
147                super::TxVerifyErr::NotSameSumOfInputsAmountAndOutputsAmount(
148                    inputs_amount,
149                    outputs_amount,
150                ),
151            );
152        }
153
154        Ok(())
155    }
156}
157
158impl TransactionDocumentV10 {
159    /// Compute  compact size in lines
160    pub fn compact_size_in_lines(&self) -> usize {
161        let compact_lines = 2  + // Header + blockstamp
162        self.issuers.len() +
163        self.inputs.len() +
164        self.unlocks.len() +
165        self.outputs.len() +
166        self.signatures.len();
167        if !self.comment.is_empty() {
168            compact_lines + 1
169        } else {
170            compact_lines
171        }
172    }
173    /// Compute transaction hash
174    pub fn compute_hash(&self) -> Hash {
175        let mut hashing_text = if let Some(ref text) = self.text {
176            text.clone()
177        } else {
178            panic!("Try to compute_hash of tx with None text !")
179        };
180        for sig in &self.signatures {
181            hashing_text.push_str(&sig.to_string());
182            hashing_text.push('\n');
183        }
184        Hash::compute(hashing_text.as_bytes())
185    }
186    /// get transaction hash option
187    pub fn get_hash_opt(&self) -> Option<Hash> {
188        self.hash
189    }
190    /// Get transaction hash
191    pub fn get_hash(&self) -> Hash {
192        if let Some(hash) = self.hash {
193            hash
194        } else {
195            self.compute_hash()
196        }
197    }
198    pub fn recipients_keys(&self) -> Vec<ed25519::PublicKey> {
199        let issuers = self.issuers.iter().copied().collect();
200        let mut pubkeys = BTreeSet::new();
201        for output in &self.outputs {
202            pubkeys.append(&mut output.conditions.script.pubkeys());
203        }
204        pubkeys.difference(&issuers).copied().collect()
205    }
206    /// Lightens the transaction (for example to store it while minimizing the space required)
207    /// WARNING: do not remove the hash as it's necessary to reverse the transaction !
208    pub fn reduce(&mut self) {
209        self.hash = Some(self.compute_hash());
210        self.text = None;
211        for output in &mut self.outputs {
212            output.reduce()
213        }
214    }
215    /// Verify comment validity
216    pub fn verify_comment(comment: &str) -> bool {
217        if comment.is_ascii() {
218            for char_ in comment.chars() {
219                match char_ {
220                    c if c.is_ascii_alphanumeric() => continue,
221                    '.' | ' ' | '-' | '_' | '\\' | ':' | '/' | ';' | '*' | '[' | ']' | '('
222                    | ')' | '?' | '!' | '^' | '+' | '=' | '@' | '&' | '~' | '#' | '{' | '}'
223                    | '|' | '<' | '>' | '%' => continue,
224                    _ => return false,
225                }
226            }
227            true
228        } else {
229            false
230        }
231    }
232    /// Indicates from which blockchain timestamp the transaction can be writable.
233    pub fn writable_on(
234        &self,
235        inputs_written_on: &[u64],
236        inputs_scripts: &[WalletScriptV10],
237    ) -> Result<u64, SourceV10NotUnlockableError> {
238        assert_eq!(self.inputs.len(), inputs_written_on.len());
239        let mut tx_unlockable_on = 0;
240        #[allow(clippy::needless_range_loop)]
241        for i in 0..self.inputs.len() {
242            let source_unlockable_on = SourceV10::unlockable_on(
243                self.issuers.as_ref(),
244                self.unlocks[i].unlocks.as_ref(),
245                inputs_written_on[i],
246                &inputs_scripts[i],
247            )?;
248            if source_unlockable_on > tx_unlockable_on {
249                tx_unlockable_on = source_unlockable_on;
250            }
251        }
252        Ok(tx_unlockable_on)
253    }
254}
255
256impl Document for TransactionDocumentV10 {
257    type PublicKey = ed25519::PublicKey;
258
259    fn version(&self) -> usize {
260        10
261    }
262
263    fn currency(&self) -> &str {
264        &self.currency
265    }
266
267    fn blockstamp(&self) -> Blockstamp {
268        self.blockstamp
269    }
270
271    fn issuers(&self) -> SmallVec<[Self::PublicKey; 1]> {
272        self.issuers.iter().copied().collect()
273    }
274
275    fn signatures(&self) -> SmallVec<[<Self::PublicKey as PublicKey>::Signature; 1]> {
276        self.signatures.iter().copied().collect()
277    }
278
279    fn as_bytes(&self) -> BeefCow<[u8]> {
280        BeefCow::borrowed(self.as_text().as_bytes())
281    }
282}
283
284impl CompactTextDocument for TransactionDocumentV10 {
285    fn as_compact_text(&self) -> String {
286        let mut issuers_str = String::from("");
287        for issuer in &self.issuers {
288            issuers_str.push('\n');
289            issuers_str.push_str(&issuer.to_string());
290        }
291        let mut inputs_str = String::from("");
292        for input in &self.inputs {
293            inputs_str.push('\n');
294            inputs_str.push_str(&input.to_string());
295        }
296        let mut unlocks_str = String::from("");
297        for unlock in &self.unlocks {
298            unlocks_str.push('\n');
299            unlocks_str.push_str(&unlock.to_string());
300        }
301        let mut outputs_str = String::from("");
302        for output in &self.outputs {
303            outputs_str.push('\n');
304            outputs_str.push_str(&output.to_string());
305        }
306        let comment_str = if self.comment.is_empty() {
307            String::with_capacity(0)
308        } else {
309            format!("{}\n", self.comment)
310        };
311        let mut signatures_str = String::from("");
312        for sig in &self.signatures {
313            signatures_str.push_str(&sig.to_string());
314            signatures_str.push('\n');
315        }
316        // Remove end line step
317        signatures_str.pop();
318        format!(
319            "TX:10:{issuers_count}:{inputs_count}:{unlocks_count}:{outputs_count}:{has_comment}:{locktime}
320{blockstamp}{issuers}{inputs}{unlocks}{outputs}\n{comment}{signatures}",
321            issuers_count = self.issuers.len(),
322            inputs_count = self.inputs.len(),
323            unlocks_count = self.unlocks.len(),
324            outputs_count = self.outputs.len(),
325            has_comment = if self.comment.is_empty() { 0 } else { 1 },
326            locktime = self.locktime,
327            blockstamp = self.blockstamp,
328            issuers = issuers_str,
329            inputs = inputs_str,
330            unlocks = unlocks_str,
331            outputs = outputs_str,
332            comment = comment_str,
333            signatures = signatures_str,
334        )
335    }
336}
337
338impl TextDocument for TransactionDocumentV10 {
339    type CompactTextDocument_ = TransactionDocumentV10;
340
341    fn as_text(&self) -> &str {
342        if let Some(ref text) = self.text {
343            text
344        } else {
345            panic!("Try to get text of tx with None text !")
346        }
347    }
348
349    fn to_compact_document(&self) -> Cow<Self::CompactTextDocument_> {
350        Cow::Borrowed(self)
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::super::tests::tx_output_v10;
357    use super::*;
358    use smallvec::smallvec;
359    use std::str::FromStr;
360    use unwrap::unwrap;
361
362    #[test]
363    fn generate_real_document() {
364        let keypair = ed25519::KeyPairFromSeed32Generator::generate(unwrap!(
365            Seed32::from_base58("DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"),
366            "Fail to parse Seed32"
367        ));
368        let pubkey = keypair.public_key();
369        let signator = keypair.generate_signator();
370
371        let sig = unwrap!(ed25519::Signature::from_base64(
372            "cq86RugQlqAEyS8zFkB9o0PlWPSb+a6D/MEnLe8j+okyFYf/WzI6pFiBkQ9PSOVn5I0dwzVXg7Q4N1apMWeGAg==",
373        ), "Fail to parse Signature");
374
375        let block = unwrap!(
376            Blockstamp::from_str(
377                "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
378            ),
379            "Fail to parse blockstamp"
380        );
381
382        let builder = TransactionDocumentV10Builder {
383            currency: "duniter_unit_test_currency",
384            blockstamp: block,
385            locktime: 0,
386            issuers: svec![pubkey],
387            inputs: &[TransactionInputV10 {
388                amount: SourceAmount::with_base0(10),
389                id: SourceIdV10::Ud(UdSourceIdV10 {
390                    issuer: unwrap!(
391                        ed25519::PublicKey::from_base58(
392                            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"
393                        ),
394                        "Fail to parse PublicKey"
395                    ),
396                    block_number: BlockNumber(0),
397                }),
398            }],
399            unlocks: &[TransactionInputUnlocksV10 {
400                index: 0,
401                unlocks: smallvec![WalletUnlockProofV10::Sig(0)],
402            }],
403            outputs: smallvec![tx_output_v10(
404                10,
405                "FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa",
406            )],
407            comment: "test",
408            hash: None,
409        };
410        assert!(builder
411            .clone()
412            .build_with_signature(svec![sig])
413            .verify_signatures()
414            .is_ok());
415        assert!(builder
416            .build_and_sign(vec![signator])
417            .verify_signatures()
418            .is_ok());
419    }
420
421    #[test]
422    fn compute_transaction_hash() {
423        let pubkey = unwrap!(
424            ed25519::PublicKey::from_base58("FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
425            "Fail to parse PublicKey"
426        );
427
428        let sig = unwrap!(ed25519::Signature::from_base64(
429            "XEwKwKF8AI1gWPT7elR4IN+bW3Qn02Dk15TEgrKtY/S2qfZsNaodsLofqHLI24BBwZ5aadpC88ntmjo/UW9oDQ==",
430        ), "Fail to parse Signature");
431
432        let block = unwrap!(
433            Blockstamp::from_str(
434                "60-00001FE00410FCD5991EDD18AA7DDF15F4C8393A64FA92A1DB1C1CA2E220128D",
435            ),
436            "Fail to parse Blockstamp"
437        );
438
439        let builder = TransactionDocumentV10Builder {
440            currency: "g1",
441            blockstamp: block,
442            locktime: 0,
443            issuers: svec![pubkey],
444            inputs: &[TransactionInputV10 {
445                amount: SourceAmount::with_base0(950),
446                id: SourceIdV10::Utxo(UtxoIdV10 {
447                    tx_hash: unwrap!(Hash::from_hex(
448                        "2CF1ACD8FE8DC93EE39A1D55881C50D87C55892AE8E4DB71D4EBAB3D412AA8FD"
449                    )),
450                    output_index: 1,
451                }),
452            }],
453            unlocks: &[TransactionInputUnlocksV10::default()],
454            outputs: smallvec![
455                tx_output_v10(30, "38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE"),
456                tx_output_v10(920, "FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
457            ],
458            comment: "Pour cesium merci",
459            hash: None,
460        };
461        let tx_doc = builder.build_with_signature(svec![sig]);
462        assert!(tx_doc.verify_signatures().is_ok());
463        assert!(tx_doc.get_hash_opt().is_none());
464        assert_eq!(
465            tx_doc.get_hash(),
466            unwrap!(Hash::from_hex(
467                "876D2430E0B66E2CE4467866D8F923D68896CACD6AA49CDD8BDD0096B834DEF1"
468            ))
469        );
470    }
471    #[test]
472    fn verify_comment() {
473        assert!(TransactionDocumentV10::verify_comment(""));
474        assert!(TransactionDocumentV10::verify_comment("sntsrttfsrt"));
475        assert!(!TransactionDocumentV10::verify_comment("sntsrt,tfsrt"));
476        assert!(TransactionDocumentV10::verify_comment("sntsrt|tfsrt"));
477    }
478
479    #[test]
480    fn tx_sign() -> Result<(), TransactionSignErr> {
481        let signator =
482            ed25519::KeyPairFromSeed32Generator::generate(Seed32::default()).generate_signator();
483
484        let tx1 = TransactionDocumentV10Builder {
485            currency: "test",
486            blockstamp: Blockstamp::default(),
487            locktime: 0,
488            issuers: svec![signator.public_key()],
489            inputs: &[],
490            unlocks: &[],
491            outputs: SmallVec::new(),
492            comment: "",
493            hash: None,
494        }
495        .build_unsigned();
496
497        if let SignedOrUnsignedDocument::Signed(tx1) = tx1.sign(&signator)? {
498            assert!(tx1.verify_signatures().is_ok());
499            Ok(())
500        } else {
501            panic!("tx1.sign should return SignedOrUnsignedDocument::Signed(_)");
502        }
503    }
504}