duniter_documents/blockchain/v10/documents/
mod.rs

1//  Copyright (C) 2018  The Duniter Project Developers.
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//! Provide wrappers around Duniter blockchain documents for protocol version 10.
17
18extern crate crypto;
19
20use self::crypto::digest::Digest;
21
22use duniter_crypto::keys::{Signature, ed25519};
23use regex::Regex;
24use blockchain::{Document, DocumentBuilder, DocumentParser};
25use blockchain::v10::documents::identity::IdentityDocumentParser;
26
27pub mod identity;
28pub mod membership;
29pub mod certification;
30pub mod revocation;
31pub mod transaction;
32pub mod block;
33
34pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder};
35pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser};
36pub use blockchain::v10::documents::certification::{CertificationDocument,
37                                                    CertificationDocumentParser};
38pub use blockchain::v10::documents::revocation::{RevocationDocument, RevocationDocumentParser};
39pub use blockchain::v10::documents::transaction::{TransactionDocument, TransactionDocumentBuilder,
40                                                  TransactionDocumentParser};
41pub use blockchain::v10::documents::block::BlockDocument;
42
43// Use of lazy_static so the regex is only compiled at first use.
44lazy_static! {
45    static ref DOCUMENT_REGEX: Regex = Regex::new(
46        "^(?P<doc>Version: (?P<version>[0-9]+)\n\
47         Type: (?P<type>[[:alpha:]]+)\n\
48         Currency: (?P<currency>[[:alnum:] _-]+)\n\
49         (?P<body>(?:.*\n)+?))\
50         (?P<sigs>([[:alnum:]+/=]+\n)*([[:alnum:]+/=]+))$"
51    ).unwrap();
52    static ref SIGNATURES_REGEX: Regex = Regex::new("[[:alnum:]+/=]+\n?").unwrap();
53}
54
55/// List of wrapped document types.
56///
57/// > TODO Add wrapped types in enum variants.
58#[derive(Debug, Clone)]
59pub enum V10Document {
60    /// Block document.
61    Block(Box<BlockDocument>),
62
63    /// Transaction document.
64    Transaction(Box<TransactionDocument>),
65
66    /// Identity document.
67    Identity(IdentityDocument),
68
69    /// Membership document.
70    Membership(MembershipDocument),
71
72    /// Certification document.
73    Certification(Box<CertificationDocument>),
74
75    /// Revocation document.
76    Revocation(Box<RevocationDocument>),
77}
78
79/// Trait for a V10 document.
80pub trait TextDocument: Document<PublicKey = ed25519::PublicKey, CurrencyType = str> {
81    /// Return document as text.
82    fn as_text(&self) -> &str;
83
84    /// Return sha256 hash of text document
85    fn hash<H: Digest>(&self, digest: &mut H) -> String {
86        digest.input_str(self.as_text());
87        digest.result_str()
88    }
89
90    /// Return document as text with leading signatures.
91    fn as_text_with_signatures(&self) -> String {
92        let mut text = self.as_text().to_string();
93
94        for sig in self.signatures() {
95            text = format!("{}{}\n", text, sig.to_base64());
96        }
97
98        text
99    }
100
101    /// Generate document compact text.
102    /// the compact format is the one used in the blocks.
103    ///
104    /// - Don't contains leading signatures
105    /// - Contains line breaks on all line.
106    fn generate_compact_text(&self) -> String;
107}
108
109/// Trait for a V10 document builder.
110pub trait TextDocumentBuilder: DocumentBuilder {
111    /// Generate document text.
112    ///
113    /// - Don't contains leading signatures
114    /// - Contains line breaks on all line.
115    fn generate_text(&self) -> String;
116
117    /// Generate final document with signatures, and also return them in an array.
118    ///
119    /// Returns :
120    ///
121    /// - Text without signatures
122    /// - Signatures
123    fn build_signed_text(
124        &self,
125        private_keys: Vec<ed25519::PrivateKey>,
126    ) -> (String, Vec<ed25519::Signature>) {
127        use duniter_crypto::keys::PrivateKey;
128
129        let text = self.generate_text();
130
131        let signatures: Vec<_> = {
132            let text_bytes = text.as_bytes();
133            private_keys
134                .iter()
135                .map(|key| key.sign(text_bytes))
136                .collect()
137        };
138
139        (text, signatures)
140    }
141}
142
143/// List of possible errors while parsing.
144#[derive(Debug, Clone)]
145pub enum V10DocumentParsingError {
146    /// The given source don't have a valid document format.
147    InvalidWrapperFormat(),
148    /// The given source don't have a valid specific document format (document type).
149    InvalidInnerFormat(String),
150    /// Type fields contains an unknown document type.
151    UnknownDocumentType(String),
152}
153
154/// V10 Documents in separated parts
155#[derive(Debug, Clone)]
156pub struct V10DocumentParts {
157    /// Whole document in text
158    pub doc: String,
159    /// Payload
160    pub body: String,
161    /// Currency
162    pub currency: String,
163    /// Signatures
164    pub signatures: Vec<ed25519::Signature>,
165}
166
167trait StandardTextDocumentParser {
168    fn parse_standard(
169        doc: &str,
170        body: &str,
171        currency: &str,
172        signatures: Vec<ed25519::Signature>,
173    ) -> Result<V10Document, V10DocumentParsingError>;
174}
175
176/// A V10 document parser.
177#[derive(Debug, Clone, Copy)]
178pub struct V10DocumentParser;
179
180impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10DocumentParser {
181    fn parse(source: &'a str) -> Result<V10Document, V10DocumentParsingError> {
182        if let Some(caps) = DOCUMENT_REGEX.captures(source) {
183            let doctype = &caps["type"];
184            let doc = &caps["doc"];
185            let currency = &caps["currency"];
186            let body = &caps["body"];
187            let sigs = SIGNATURES_REGEX
188                .captures_iter(&caps["sigs"])
189                .map(|capture| ed25519::Signature::from_base64(&capture[0]).unwrap())
190                .collect::<Vec<_>>();
191
192            // TODO : Improve error handling of Signature::from_base64 failure
193
194            match doctype {
195                "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs),
196                "Membership" => MembershipDocumentParser::parse_standard(doc, body, currency, sigs),
197                "Certification" => {
198                    CertificationDocumentParser::parse_standard(doc, body, currency, sigs)
199                }
200                "Revocation" => RevocationDocumentParser::parse_standard(doc, body, currency, sigs),
201                "Transaction" => {
202                    TransactionDocumentParser::parse_standard(doc, body, currency, sigs)
203                }
204                _ => Err(V10DocumentParsingError::UnknownDocumentType(
205                    doctype.to_string(),
206                )),
207            }
208        } else {
209            Err(V10DocumentParsingError::InvalidWrapperFormat())
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use blockchain::{Document, VerificationResult};
218
219    #[test]
220    fn document_regex() {
221        assert!(DOCUMENT_REGEX.is_match(
222            "Version: 10
223Type: Transaction
224Currency: beta_brousouf
225Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B
226Locktime: 0
227Issuers:
228HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY
229CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp
2309WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB
231Inputs:
23240:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2
23370:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8
23420:2:D:HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY:46
23570:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3
23620:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5
23715:2:D:9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:46
238Unlocks:
2390:SIG(0)
2401:XHX(7665798292)
2412:SIG(0)
2423:SIG(0) SIG(2)
2434:SIG(0) SIG(1) SIG(2)
2445:SIG(2)
245Outputs:
246120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)
247146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)
24849:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i)\
249 || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
250Comment: -----@@@----- (why not this comment?)
25142yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
2522D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
2532XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk"
254        ));
255
256        assert!(DOCUMENT_REGEX.is_match(
257            "Version: 10
258Type: Certification
259Currency: beta_brousouf
260Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
261IdtyIssuer: HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd
262IdtyUniqueID: lolcat
263IdtyTimestamp: 32-DB30D958EE5CB75186972286ED3F4686B8A1C2CD
264IdtySignature: J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUb\
265GpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci
266CertTimestamp: 36-1076F10A7397715D2BEE82579861999EA1F274AC
267SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneNYSMV3rk"
268        ));
269    }
270
271    #[test]
272    fn signatures_regex() {
273        assert_eq!(
274            SIGNATURES_REGEX
275                .captures_iter(
276                    "
27742yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
2782D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
2792XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk"
280                )
281                .count(),
282            3
283        );
284
285        assert_eq!(
286            SIGNATURES_REGEX
287                .captures_iter(
288                    "
28942yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
2902XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk"
291                )
292                .count(),
293            2
294        );
295    }
296
297    #[test]
298    fn parse_identity_document() {
299        let text = "Version: 10
300Type: Identity
301Currency: g1
302Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx
303UniqueID: elois
304Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
305Ydnclvw76/JHcKSmU9kl9Ie0ne5/X8NYOqPqbGnufIK3eEPRYYdEYaQh+zffuFhbtIRjv6m/DkVLH5cLy/IyAg==";
306
307        let doc = V10DocumentParser::parse(text).unwrap();
308        if let V10Document::Identity(doc) = doc {
309            println!("Doc : {:?}", doc);
310            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
311        } else {
312            panic!("Wrong document type");
313        }
314    }
315
316    #[test]
317    fn parse_membership_document() {
318        let text = "Version: 10
319Type: Membership
320Currency: g1
321Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx
322Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
323Membership: IN
324UserID: elois
325CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
326FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg==";
327
328        let doc = V10DocumentParser::parse(text).unwrap();
329        if let V10Document::Membership(doc) = doc {
330            println!("Doc : {:?}", doc);
331            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
332        } else {
333            panic!("Wrong document type");
334        }
335    }
336
337    #[test]
338    fn parse_certification_document() {
339        let text = "Version: 10
340Type: Certification
341Currency: g1
342Issuer: 2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT
343IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9
344IdtyUniqueID: fbarbut
345IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3
346IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ==
347CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD
348Hkps1QU4HxIcNXKT8YmprYTVByBhPP1U2tIM7Z8wENzLKIWAvQClkAvBE7pW9dnVa18sJIJhVZUcRrPAZfmjBA==";
349
350        let doc = V10DocumentParser::parse(text).unwrap();
351        if let V10Document::Certification(doc) = doc {
352            println!("Doc : {:?}", doc);
353            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
354        } else {
355            panic!("Wrong document type");
356        }
357    }
358
359    #[test]
360    fn parse_revocation_document() {
361        let text = "Version: 10
362Type: Revocation
363Currency: g1
364Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
365IdtyUniqueID: tic
366IdtyTimestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
367IdtySignature: 1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==
368XXOgI++6qpY9O31ml/FcfbXCE6aixIrgkT5jL7kBle3YOMr+8wrp7Rt+z9hDVjrNfYX2gpeJsuMNfG4T/fzVDQ==";
369
370        let doc = V10DocumentParser::parse(text).unwrap();
371        if let V10Document::Revocation(doc) = doc {
372            println!("Doc : {:?}", doc);
373            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
374        } else {
375            panic!("Wrong document type");
376        }
377    }
378
379    #[test]
380    fn parse_transaction_document() {
381        let text = "Version: 10
382Type: Transaction
383Currency: g1
384Blockstamp: 107702-0000017CDBE974DC9A46B89EE7DC2BEE4017C43A005359E0853026C21FB6A084
385Locktime: 0
386Issuers:
387Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC
388Inputs:
3891002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:104937
3901002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:105214
391Unlocks:
3920:SIG(0)
3931:SIG(0)
394Outputs:
3952004:0:SIG(DTgQ97AuJ8UgVXcxmNtULAs8Fg1kKC1Wr9SAS96Br9NG)
396Comment: c est pour 2 mois d adhesion ressourcerie
397lnpuFsIymgz7qhKF/GsZ3n3W8ZauAAfWmT4W0iJQBLKJK2GFkesLWeMj/+GBfjD6kdkjreg9M6VfkwIZH+hCCQ==";
398
399        let doc = V10DocumentParser::parse(text).unwrap();
400        if let V10Document::Transaction(doc) = doc {
401            println!("Doc : {:?}", doc);
402            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
403        } else {
404            panic!("Wrong document type");
405        }
406    }
407}