dubp_documents/
transaction.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
18pub mod v10;
19pub mod v10_gen;
20
21use crate::*;
22
23pub use v10::{
24    TransactionDocumentV10, TransactionDocumentV10Builder, TransactionDocumentV10Stringified,
25    TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10,
26    UnsignedTransactionDocumentV10,
27};
28
29/// Wrap an utxo conditions
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
31pub struct UTXOConditions {
32    /// We are obliged to allow the introduction of the original text (instead of the self-generated text),
33    /// because the original text may contain errors that are unfortunately allowed by duniter-ts.
34    pub origin_str: Option<String>,
35    /// Store script conditions
36    pub script: WalletScriptV10,
37}
38
39impl From<WalletScriptV10> for UTXOConditions {
40    fn from(script: WalletScriptV10) -> Self {
41        UTXOConditions {
42            origin_str: None,
43            script,
44        }
45    }
46}
47
48impl UTXOConditions {
49    /// Lightens the UTXOConditions (for example to store it while minimizing the space required)
50    pub fn reduce(&mut self) {
51        if self.check() {
52            self.origin_str = None;
53        }
54    }
55    /// Check validity of this UTXOConditions
56    pub fn check(&self) -> bool {
57        if let Some(ref origin_str) = self.origin_str {
58            origin_str == self.script.to_string().as_str()
59        } else {
60            true
61        }
62    }
63}
64
65impl ToString for UTXOConditions {
66    fn to_string(&self) -> String {
67        if let Some(ref origin_str) = self.origin_str {
68            origin_str.to_string()
69        } else {
70            self.script.to_string()
71        }
72    }
73}
74
75#[derive(Debug, Error)]
76pub enum TxVerifyErr {
77    #[error("Transaction must have at least one input")]
78    NoInput,
79    #[error("Transaction must have at least one issuer")]
80    NoIssuer,
81    #[error("Transaction must have at least one output")]
82    NoOutput,
83    #[error("Not same amount of inputs and unlocks: found {0} inputs and {1} unlocks.")]
84    NotSameAmountOfInputsAndUnlocks(usize, usize),
85    #[error("Not same sum of inputs amount and outputs amount: ({0}, {1}).")]
86    NotSameSumOfInputsAmountAndOutputsAmount(SourceAmount, SourceAmount),
87    #[error("{0}")]
88    Sigs(DocumentSigsErr),
89    #[error("Transaction too long: found {found} lines (max {max}).")]
90    TooManyLines { found: usize, max: usize },
91    #[error("Wrong currency: expected \'{expected}\' found \'{found}\'.")]
92    WrongCurrency { expected: String, found: String },
93}
94
95#[derive(Clone, Copy, Debug, Error)]
96pub enum GenTxError {
97    #[error("Too Many signers or recipients.")]
98    TooManySignersOrRecipients,
99}
100
101pub trait UnsignedTransactionDocumentTrait<'a>: Sized {
102    type PubKey: PublicKey;
103    type SignedDoc: TransactionDocumentTrait<'a>;
104
105    /// Get transaction raw text
106    fn as_text(&self) -> &str;
107
108    /// Sign the document
109    fn sign<S: Signator<PublicKey = Self::PubKey>>(
110        self,
111        signator: &S,
112    ) -> Result<SignedOrUnsignedDocument<Self::SignedDoc, Self>, TransactionSignErr>;
113}
114
115pub trait TransactionDocumentTrait<'a>: Sized {
116    type Address;
117    type Input: 'a;
118    type Inputs: AsRef<[Self::Input]>;
119    type InputUnlocks: 'a;
120    type InputsUnlocks: AsRef<[Self::InputUnlocks]>;
121    type Output: 'a;
122    type Outputs: AsRef<[Self::Output]>;
123    type PubKey: PublicKey;
124    type UnsignedDoc: UnsignedTransactionDocumentTrait<'a>;
125
126    fn generate_simple_txs(
127        blockstamp: Blockstamp,
128        currency: String,
129        inputs_with_sum: (Vec<Self::Input>, SourceAmount),
130        issuer: Self::PubKey,
131        recipient: WalletScriptV10,
132        user_amount_and_comment: (SourceAmount, String),
133        cash_back_pubkey: Option<Self::PubKey>,
134    ) -> Vec<Self::UnsignedDoc>;
135    fn get_inputs(&'a self) -> Self::Inputs;
136    fn get_inputs_unlocks(&'a self) -> Self::InputsUnlocks;
137    fn get_outputs(&'a self) -> Self::Outputs;
138    fn verify(&self, expected_currency: Option<&str>) -> Result<(), TxVerifyErr>;
139}
140
141/// List of possible errors when signing a transaction document.
142#[derive(Debug, Error, Eq, PartialEq)]
143pub enum TransactionSignErr {
144    /// The signator's public key is not in the transaction's issuers
145    #[error("The signator's public key is not in the transaction's issuers")]
146    InvalidSignator,
147    /// Signatures don't match.
148    /// List of mismatching pairs indexes.
149    #[error("Signatures don\'t match: {0:?}")]
150    InvalidSignatures(HashMap<usize, SigError>),
151}
152
153/// Wrap a Transaction document.
154///
155/// Must be created by parsing a text document or using a builder.
156#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
157pub enum TransactionDocument {
158    V10(TransactionDocumentV10),
159}
160
161#[derive(Clone, Debug, Deserialize, Hash, Serialize, PartialEq, Eq)]
162/// Transaction document stringifed
163pub enum TransactionDocumentStringified {
164    V10(TransactionDocumentV10Stringified),
165}
166
167impl ToStringObject for TransactionDocument {
168    type StringObject = TransactionDocumentStringified;
169
170    fn to_string_object(&self) -> TransactionDocumentStringified {
171        match self {
172            TransactionDocument::V10(tx_v10) => {
173                TransactionDocumentStringified::V10(tx_v10.to_string_object())
174            }
175        }
176    }
177}
178
179impl TransactionDocument {
180    /// Compute transaction hash
181    pub fn compute_hash(&self) -> Hash {
182        match self {
183            TransactionDocument::V10(tx_v10) => tx_v10.compute_hash(),
184        }
185    }
186    /// get transaction hash option
187    pub fn get_hash_opt(&self) -> Option<Hash> {
188        match self {
189            TransactionDocument::V10(tx_v10) => tx_v10.get_hash_opt(),
190        }
191    }
192    /// Get transaction hash
193    pub fn get_hash(&mut self) -> Hash {
194        match self {
195            TransactionDocument::V10(tx_v10) => tx_v10.get_hash(),
196        }
197    }
198    /// Lightens the transaction (for example to store it while minimizing the space required)
199    /// WARNING: do not remove the hash as it's necessary to reverse the transaction !
200    pub fn reduce(&mut self) {
201        match self {
202            TransactionDocument::V10(tx_v10) => tx_v10.reduce(),
203        };
204    }
205}
206
207impl Document for TransactionDocument {
208    type PublicKey = PubKeyEnum;
209
210    fn version(&self) -> usize {
211        match self {
212            TransactionDocument::V10(tx_v10) => tx_v10.version(),
213        }
214    }
215
216    fn currency(&self) -> &str {
217        match self {
218            TransactionDocument::V10(tx_v10) => tx_v10.currency(),
219        }
220    }
221
222    fn blockstamp(&self) -> Blockstamp {
223        match self {
224            TransactionDocument::V10(tx_v10) => tx_v10.blockstamp(),
225        }
226    }
227
228    fn issuers(&self) -> SmallVec<[Self::PublicKey; 1]> {
229        match self {
230            TransactionDocument::V10(tx_v10) => svec![PubKeyEnum::Ed25519(tx_v10.issuers()[0])],
231        }
232    }
233
234    fn signatures(&self) -> SmallVec<[<Self::PublicKey as PublicKey>::Signature; 1]> {
235        match self {
236            TransactionDocument::V10(tx_v10) => svec![Sig::Ed25519(tx_v10.signatures()[0])],
237        }
238    }
239
240    fn as_bytes(&self) -> BeefCow<[u8]> {
241        match self {
242            TransactionDocument::V10(tx_v10) => tx_v10.as_bytes(),
243        }
244    }
245}
246
247impl CompactTextDocument for TransactionDocument {
248    fn as_compact_text(&self) -> String {
249        match self {
250            TransactionDocument::V10(tx_v10) => tx_v10.as_compact_text(),
251        }
252    }
253}
254
255impl TextDocument for TransactionDocument {
256    type CompactTextDocument_ = TransactionDocument;
257
258    fn as_text(&self) -> &str {
259        match self {
260            TransactionDocument::V10(tx_v10) => tx_v10.as_text(),
261        }
262    }
263
264    fn to_compact_document(&self) -> Cow<Self::CompactTextDocument_> {
265        Cow::Borrowed(self)
266    }
267}
268
269/// Transaction document builder.
270#[derive(Debug, Clone)]
271pub enum TransactionDocumentBuilder<'a> {
272    V10(TransactionDocumentV10Builder<'a>),
273}
274
275impl<'a> TextDocumentBuilder for TransactionDocumentBuilder<'a> {
276    type Document = TransactionDocument;
277    type Signator = SignatorEnum;
278
279    fn build_with_text_and_sigs(
280        self,
281        text: String,
282        signatures: SmallVec<
283            [<<Self::Document as Document>::PublicKey as PublicKey>::Signature; 1],
284        >,
285    ) -> TransactionDocument {
286        match self {
287            TransactionDocumentBuilder::V10(tx_v10_builder) => TransactionDocument::V10(
288                tx_v10_builder.build_with_text_and_sigs(
289                    text,
290                    signatures
291                        .into_iter()
292                        .filter_map(|sig| {
293                            if let Sig::Ed25519(sig) = sig {
294                                Some(sig)
295                            } else {
296                                None
297                            }
298                        })
299                        .collect(),
300                ),
301            ),
302        }
303    }
304
305    fn generate_text(&self) -> String {
306        match self {
307            TransactionDocumentBuilder::V10(tx_v10_builder) => tx_v10_builder.generate_text(),
308        }
309    }
310}
311
312#[cfg(test)]
313pub(super) mod tests {
314    use super::*;
315    use smallvec::smallvec;
316    use std::str::FromStr;
317    use unwrap::unwrap;
318    use v10::{TransactionInputUnlocksV10, TransactionOutputV10};
319
320    pub(super) fn tx_output_v10(amount: i64, recv: &str) -> TransactionOutputV10 {
321        TransactionOutputV10 {
322            amount: SourceAmount::with_base0(amount),
323            conditions: UTXOConditions::from(WalletScriptV10::single(WalletConditionV10::Sig(
324                unwrap!(ed25519::PublicKey::from_base58(recv)),
325            ))),
326        }
327    }
328
329    #[test]
330    fn generate_real_document() {
331        let keypair = ed25519::KeyPairFromSeed32Generator::generate(unwrap!(
332            Seed32::from_base58("DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"),
333            "Fail to parse Seed32"
334        ));
335        let pubkey = keypair.public_key();
336        let signator = keypair.generate_signator();
337
338        let sig = unwrap!(ed25519::Signature::from_base64(
339            "cq86RugQlqAEyS8zFkB9o0PlWPSb+a6D/MEnLe8j+okyFYf/WzI6pFiBkQ9PSOVn5I0dwzVXg7Q4N1apMWeGAg==",
340        ), "Fail to parse Signature");
341
342        let blockstamp = unwrap!(
343            Blockstamp::from_str(
344                "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
345            ),
346            "Fail to parse blockstamp"
347        );
348
349        let builder = TransactionDocumentV10Builder {
350            currency: "duniter_unit_test_currency",
351            blockstamp,
352            locktime: 0,
353            issuers: svec![pubkey],
354            inputs: &[TransactionInputV10 {
355                amount: SourceAmount::with_base0(10),
356                id: SourceIdV10::Ud(UdSourceIdV10 {
357                    issuer: unwrap!(
358                        ed25519::PublicKey::from_base58(
359                            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"
360                        ),
361                        "Fail to parse PublicKey"
362                    ),
363                    block_number: BlockNumber(0),
364                }),
365            }],
366            unlocks: &[TransactionInputUnlocksV10 {
367                index: 0,
368                unlocks: svec![WalletUnlockProofV10::Sig(0)],
369            }],
370            outputs: smallvec![tx_output_v10(
371                10,
372                "FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa",
373            )],
374            comment: "test",
375            hash: None,
376        };
377        assert!(builder
378            .clone()
379            .build_with_signature(svec![sig])
380            .verify_signatures()
381            .is_ok());
382        assert!(builder
383            .build_and_sign(vec![signator])
384            .verify_signatures()
385            .is_ok());
386    }
387
388    #[test]
389    fn compute_transaction_hash() {
390        let pubkey = unwrap!(
391            ed25519::PublicKey::from_base58("FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
392            "Fail to parse PublicKey"
393        );
394
395        let sig = unwrap!(ed25519::Signature::from_base64(
396            "XEwKwKF8AI1gWPT7elR4IN+bW3Qn02Dk15TEgrKtY/S2qfZsNaodsLofqHLI24BBwZ5aadpC88ntmjo/UW9oDQ==",
397        ), "Fail to parse Signature");
398
399        let blockstamp = unwrap!(
400            Blockstamp::from_str(
401                "60-00001FE00410FCD5991EDD18AA7DDF15F4C8393A64FA92A1DB1C1CA2E220128D",
402            ),
403            "Fail to parse Blockstamp"
404        );
405
406        let builder = TransactionDocumentV10Builder {
407            currency: "g1",
408            blockstamp,
409            locktime: 0,
410            issuers: svec![pubkey],
411            inputs: &[TransactionInputV10 {
412                amount: SourceAmount::with_base0(950),
413                id: SourceIdV10::Utxo(UtxoIdV10 {
414                    tx_hash: unwrap!(Hash::from_hex(
415                        "2CF1ACD8FE8DC93EE39A1D55881C50D87C55892AE8E4DB71D4EBAB3D412AA8FD"
416                    )),
417                    output_index: 1,
418                }),
419            }],
420            unlocks: &[TransactionInputUnlocksV10::default()],
421            outputs: smallvec![
422                tx_output_v10(30, "38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE"),
423                tx_output_v10(920, "FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
424            ],
425            comment: "Pour cesium merci",
426            hash: None,
427        };
428        let mut tx_doc = TransactionDocument::V10(builder.build_with_signature(svec![sig]));
429        assert!(tx_doc.verify_signatures().is_ok());
430        assert!(tx_doc.get_hash_opt().is_none());
431        assert_eq!(
432            tx_doc.get_hash(),
433            unwrap!(Hash::from_hex(
434                "876D2430E0B66E2CE4467866D8F923D68896CACD6AA49CDD8BDD0096B834DEF1"
435            ))
436        );
437    }
438}