duniter_documents/blockchain/v10/documents/
mod.rs1extern 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
43lazy_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#[derive(Debug, Clone)]
59pub enum V10Document {
60 Block(Box<BlockDocument>),
62
63 Transaction(Box<TransactionDocument>),
65
66 Identity(IdentityDocument),
68
69 Membership(MembershipDocument),
71
72 Certification(Box<CertificationDocument>),
74
75 Revocation(Box<RevocationDocument>),
77}
78
79pub trait TextDocument: Document<PublicKey = ed25519::PublicKey, CurrencyType = str> {
81 fn as_text(&self) -> &str;
83
84 fn hash<H: Digest>(&self, digest: &mut H) -> String {
86 digest.input_str(self.as_text());
87 digest.result_str()
88 }
89
90 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 fn generate_compact_text(&self) -> String;
107}
108
109pub trait TextDocumentBuilder: DocumentBuilder {
111 fn generate_text(&self) -> String;
116
117 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#[derive(Debug, Clone)]
145pub enum V10DocumentParsingError {
146 InvalidWrapperFormat(),
148 InvalidInnerFormat(String),
150 UnknownDocumentType(String),
152}
153
154#[derive(Debug, Clone)]
156pub struct V10DocumentParts {
157 pub doc: String,
159 pub body: String,
161 pub currency: String,
163 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#[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 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}