dubp_block/block/
v10.rs

1//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
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 Block document V10.
17
18use crate::*;
19use dubp_documents::identity::IdentityDocumentV10;
20use dubp_documents::membership::v10::MembershipDocumentV10;
21use dubp_documents::revocation::{CompactRevocationDocumentV10, RevocationDocumentV10};
22use dubp_documents::transaction::v10::{TransactionDocumentV10, TransactionDocumentV10Stringified};
23use dubp_documents::{
24    certification::{v10::CertificationDocumentV10, CompactCertificationDocumentV10},
25    dubp_wallet::prelude::SourceAmount,
26};
27use std::borrow::Cow;
28
29#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
30pub struct DubpBlockV10Content {
31    /// Version
32    pub version: usize,
33    /// number
34    pub number: BlockNumber,
35    /// Minimal proof of work difficulty
36    pub pow_min: usize,
37    /// Local time of the block issuer
38    pub time: u64,
39    /// Average time
40    pub median_time: u64,
41    /// Members count
42    pub members_count: usize,
43    /// Monetary mass
44    pub monetary_mass: u64,
45    /// Unit base (power of ten)
46    pub unit_base: usize,
47    /// Number of compute members in the current frame
48    pub issuers_count: usize,
49    /// Current frame size (in blocks)
50    pub issuers_frame: usize,
51    /// Current frame variation buffer
52    pub issuers_frame_var: isize,
53    /// Currency.
54    pub currency: CurrencyName,
55    /// Block issuer
56    pub issuer: ed25519::PublicKey,
57    /// Currency parameters (only in genesis block)
58    pub parameters: Option<BlockV10Parameters>,
59    /// Hash of the previous block
60    pub previous_hash: Hash,
61    /// Issuer of the previous block
62    pub previous_issuer: ed25519::PublicKey,
63    /// Amount of new dividend created at this block, None if no dividend is created at this block
64    pub dividend: Option<usize>,
65    /// Identities
66    pub identities: Vec<IdentityDocumentV10>,
67    /// joiners
68    pub joiners: Vec<MembershipDocumentV10>,
69    /// Actives (=renewals)
70    pub actives: Vec<MembershipDocumentV10>,
71    /// Leavers
72    pub leavers: Vec<MembershipDocumentV10>,
73    /// Revokeds
74    pub revoked: Vec<TextDocumentFormat<RevocationDocumentV10>>,
75    /// Excludeds
76    pub excluded: Vec<ed25519::PublicKey>,
77    /// Certifications
78    pub certifications: Vec<TextDocumentFormat<CertificationDocumentV10>>,
79    /// Transactions
80    pub transactions: Vec<TransactionDocumentV10>,
81}
82
83impl DubpBlockV10Content {
84    pub(crate) fn gen_hashable_text(&self) -> String {
85        let mut identities_str = String::from("");
86        for identity in &self.identities {
87            identities_str.push('\n');
88            identities_str.push_str(&identity.generate_compact_text());
89        }
90        let mut joiners_str = String::new();
91        for joiner in &self.joiners {
92            joiners_str.push('\n');
93            joiners_str.push_str(&joiner.generate_compact_text());
94        }
95        let mut actives_str = String::new();
96        for active in &self.actives {
97            actives_str.push('\n');
98            actives_str.push_str(&active.generate_compact_text());
99        }
100        let mut leavers_str = String::new();
101        for leaver in &self.leavers {
102            leavers_str.push('\n');
103            leavers_str.push_str(&leaver.generate_compact_text());
104        }
105        let mut identities_str = String::new();
106        for identity in &self.identities {
107            identities_str.push('\n');
108            identities_str.push_str(&identity.generate_compact_text());
109        }
110        let mut revokeds_str = String::new();
111        for revocation in &self.revoked {
112            revokeds_str.push('\n');
113            revokeds_str.push_str(&revocation.as_compact_text());
114        }
115        let mut excludeds_str = String::new();
116        for exclusion in &self.excluded {
117            excludeds_str.push('\n');
118            excludeds_str.push_str(&exclusion.to_string());
119        }
120        let mut certifications_str = String::new();
121        for certification in &self.certifications {
122            certifications_str.push('\n');
123            certifications_str.push_str(&certification.as_compact_text());
124        }
125        let mut transactions_str = String::new();
126        for transaction in &self.transactions {
127            transactions_str.push('\n');
128            transactions_str.push_str(&transaction.generate_compact_text());
129        }
130        let mut dividend_str = String::new();
131        if let Some(dividend) = self.dividend {
132            if dividend > 0 {
133                dividend_str.push_str("UniversalDividend: ");
134                dividend_str.push_str(&dividend.to_string());
135                dividend_str.push('\n');
136            }
137        }
138        let mut parameters_str = String::new();
139        if let Some(params) = self.parameters {
140            parameters_str.push_str("Parameters: ");
141            parameters_str.push_str(&params.to_string());
142            parameters_str.push('\n');
143        }
144        let mut previous_hash_str = String::new();
145        if self.number.0 > 0 {
146            previous_hash_str.push_str("PreviousHash: ");
147            previous_hash_str.push_str(&self.previous_hash.to_string());
148            previous_hash_str.push('\n');
149        }
150        let mut previous_issuer_str = String::new();
151        if self.number.0 > 0 {
152            previous_issuer_str.push_str("PreviousIssuer: ");
153            previous_issuer_str.push_str(&self.previous_issuer.to_string());
154            previous_issuer_str.push('\n');
155        }
156        format!(
157            "Version: {version}
158Type: Block
159Currency: {currency}
160Number: {block_number}
161PoWMin: {pow_min}
162Time: {time}
163MedianTime: {median_time}
164{dividend}UnitBase: {unit_base}
165Issuer: {issuer}
166IssuersFrame: {issuers_frame}
167IssuersFrameVar: {issuers_frame_var}
168DifferentIssuersCount: {issuers_count}
169{parameters}{previous_hash}{previous_issuer}MembersCount: {members_count}
170Identities:{identities}
171Joiners:{joiners}
172Actives:{actives}
173Leavers:{leavers}
174Revoked:{revoked}
175Excluded:{excluded}
176Certifications:{certifications}
177Transactions:{transactions}
178",
179            version = self.version,
180            currency = self.currency,
181            block_number = self.number,
182            pow_min = self.pow_min,
183            time = self.time,
184            median_time = self.median_time,
185            dividend = dividend_str,
186            unit_base = self.unit_base,
187            issuer = self.issuer,
188            issuers_frame = self.issuers_frame,
189            issuers_frame_var = self.issuers_frame_var,
190            issuers_count = self.issuers_count,
191            parameters = parameters_str,
192            previous_hash = previous_hash_str,
193            previous_issuer = previous_issuer_str,
194            members_count = self.members_count,
195            identities = identities_str,
196            joiners = joiners_str,
197            actives = actives_str,
198            leavers = leavers_str,
199            revoked = revokeds_str,
200            excluded = excludeds_str,
201            certifications = certifications_str,
202            transactions = transactions_str,
203        )
204    }
205}
206
207#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)]
208pub struct DubpBlockV10AfterPowData {
209    pub nonce: u64,
210    pub signature: ed25519::Signature,
211    pub hash: BlockHash,
212}
213#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
214pub struct DubpBlockV10Builder {
215    /// Block content
216    content: DubpBlockV10Content,
217    /// Block inner hash (=hash of content)
218    inner_hash: Hash,
219}
220
221impl DubpBlockV10Builder {
222    pub fn new(content: DubpBlockV10Content) -> Self {
223        DubpBlockV10Builder {
224            inner_hash: Hash::compute(content.gen_hashable_text().as_bytes()),
225            content,
226        }
227    }
228    pub fn inner_hash(&self) -> Hash {
229        self.inner_hash
230    }
231    pub fn build_unchecked(self, after_pow_data: DubpBlockV10AfterPowData) -> DubpBlockV10 {
232        DubpBlockV10 {
233            content: self.content,
234            inner_hash: Some(self.inner_hash),
235            nonce: after_pow_data.nonce,
236            signature: after_pow_data.signature,
237            hash: after_pow_data.hash,
238        }
239    }
240}
241
242/// Wrap a Block document.
243///
244/// Must be created by parsing/deserialization or using a builder.
245#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
246pub struct DubpBlockV10 {
247    /// Block content
248    content: DubpBlockV10Content,
249    /// Block inner hash (=hash of content)
250    /// Reduced block contains None because this field can be computed and checked with block hash
251    inner_hash: Option<Hash>,
252    /// Nonce
253    nonce: u64,
254    /// Block signature
255    signature: ed25519::Signature,
256    /// Block hash
257    hash: BlockHash,
258}
259
260impl DubpBlockTrait for DubpBlockV10 {
261    type Signator = ed25519::Signator;
262
263    fn common_time(&self) -> u64 {
264        self.content.median_time
265    }
266    fn compute_hashed_string(&self) -> String {
267        format!("{}{}\n", self.compute_signed_string(), self.signature)
268    }
269    fn compute_signed_string(&self) -> String {
270        let inner_hash = if let Some(inner_hash) = self.inner_hash {
271            inner_hash
272        } else {
273            self.compute_inner_hash()
274        };
275        format!(
276            "InnerHash: {}\nNonce: {}\n",
277            inner_hash.to_hex(),
278            self.nonce
279        )
280    }
281    fn current_frame_size(&self) -> usize {
282        self.content.issuers_frame
283    }
284    fn currency_name(&self) -> CurrencyName {
285        self.content.currency.clone()
286    }
287    fn currency_parameters(&self) -> Option<CurrencyParameters> {
288        if let Some(genesis_parameters) = self.content.parameters {
289            Some(CurrencyParameters::from((
290                &self.content.currency,
291                genesis_parameters,
292            )))
293        } else {
294            None
295        }
296    }
297    fn dividend(&self) -> Option<SourceAmount> {
298        if let Some(dividend) = self.content.dividend {
299            Some(SourceAmount::new(
300                dividend as i64,
301                self.content.unit_base as i64,
302            ))
303        } else {
304            None
305        }
306    }
307    fn generate_compact_inner_text(&self) -> String {
308        self.content.gen_hashable_text()
309    }
310    fn hash(&self) -> BlockHash {
311        self.hash
312    }
313    fn inner_hash(&self) -> Hash {
314        if let Some(inner_hash) = self.inner_hash {
315            inner_hash
316        } else {
317            self.compute_inner_hash()
318        }
319    }
320    fn issuers_count(&self) -> usize {
321        self.content.issuers_count
322    }
323    fn issuers_frame(&self) -> usize {
324        self.content.issuers_frame
325    }
326    fn issuer(&self) -> ed25519::PublicKey {
327        self.content.issuer
328    }
329    fn local_time(&self) -> u64 {
330        self.content.time
331    }
332    fn members_count(&self) -> usize {
333        self.content.members_count
334    }
335    fn monetary_mass(&self) -> u64 {
336        self.content.monetary_mass
337    }
338    fn nonce(&self) -> u64 {
339        self.nonce
340    }
341    fn number(&self) -> BlockNumber {
342        self.content.number
343    }
344    fn pow_min(&self) -> usize {
345        self.content.pow_min
346    }
347    fn previous_blockstamp(&self) -> Blockstamp {
348        if self.content.number.0 > 0 {
349            Blockstamp {
350                number: BlockNumber(self.content.number.0 - 1),
351                hash: BlockHash(self.content.previous_hash),
352            }
353        } else {
354            Blockstamp::default()
355        }
356    }
357    fn previous_hash(&self) -> Hash {
358        self.content.previous_hash
359    }
360    fn reduce(&mut self) {
361        //self.hash = None;
362        self.inner_hash = None;
363        for i in &mut self.content.identities {
364            i.reduce();
365        }
366        for i in &mut self.content.joiners {
367            i.reduce();
368        }
369        for i in &mut self.content.actives {
370            i.reduce();
371        }
372        for i in &mut self.content.leavers {
373            i.reduce();
374        }
375        for i in &mut self.content.transactions {
376            i.reduce();
377        }
378    }
379    fn sign(&mut self, signator: &Self::Signator) -> Result<(), SignError> {
380        self.signature = signator.sign(self.compute_signed_string().as_bytes());
381        Ok(())
382    }
383    fn signature(&self) -> ed25519::Signature {
384        self.signature
385    }
386    fn verify_inner_hash(&self) -> Result<(), VerifyBlockHashError> {
387        match self.inner_hash {
388            Some(inner_hash) => {
389                let computed_hash = self.compute_inner_hash();
390                if inner_hash == computed_hash {
391                    Ok(())
392                } else {
393                    Err(VerifyBlockHashError::InvalidHash {
394                        block_number: self.content.number,
395                        expected_hash: computed_hash,
396                        actual_hash: inner_hash,
397                    })
398                }
399            }
400            None => Err(VerifyBlockHashError::MissingHash {
401                block_number: self.content.number,
402            }),
403        }
404    }
405    fn verify_signature(&self) -> Result<(), SigError> {
406        self.content
407            .issuer
408            .verify(self.compute_signed_string().as_bytes(), &self.signature)
409    }
410    fn verify_hash(&self) -> Result<(), VerifyBlockHashError> {
411        let expected_hash = self.compute_hash();
412        if self.hash == expected_hash {
413            Ok(())
414        } else {
415            warn!(
416                "Block #{} have invalid hash (expected='{}', actual='{}', datas='{}').",
417                self.content.number.0,
418                expected_hash,
419                self.hash,
420                self.compute_hashed_string()
421            );
422            Err(VerifyBlockHashError::InvalidHash {
423                block_number: self.content.number,
424                expected_hash: expected_hash.0,
425                actual_hash: self.hash.0,
426            })
427        }
428    }
429    fn unit_base(&self) -> usize {
430        self.content.unit_base
431    }
432}
433
434impl DubpBlockV10 {
435    pub fn identities(&self) -> &[IdentityDocumentV10] {
436        &self.content.identities
437    }
438    pub fn joiners(&self) -> &[MembershipDocumentV10] {
439        &self.content.joiners
440    }
441    pub fn actives(&self) -> &[MembershipDocumentV10] {
442        &self.content.actives
443    }
444    pub fn leavers(&self) -> &[MembershipDocumentV10] {
445        &self.content.leavers
446    }
447    pub fn revoked(&self) -> Vec<Cow<CompactRevocationDocumentV10>> {
448        self.content
449            .revoked
450            .iter()
451            .map(|revo| revo.to_compact_document())
452            .collect()
453    }
454    pub fn excluded(&self) -> &[ed25519::PublicKey] {
455        &self.content.excluded
456    }
457    pub fn certifications(&self) -> Vec<Cow<CompactCertificationDocumentV10>> {
458        self.content
459            .certifications
460            .iter()
461            .map(|cert| cert.to_compact_document())
462            .collect()
463    }
464    pub fn transactions(&self) -> &[TransactionDocumentV10] {
465        &self.content.transactions
466    }
467    /// Needed only for BMA (to be removed)
468    #[cfg(not(tarpaulin_include))]
469    pub fn as_compact_text(&self) -> String {
470        let compact_inner_text = self.generate_compact_inner_text();
471        let inner_hash = if let Some(inner_hash) = self.inner_hash {
472            inner_hash
473        } else {
474            Hash::compute(compact_inner_text.as_bytes())
475        };
476        format!(
477            "{}InnerHash: {}\nNonce: ",
478            compact_inner_text,
479            inner_hash.to_hex()
480        )
481    }
482}
483
484#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
485#[serde(rename_all = "camelCase")]
486pub struct DubpBlockV10Stringified {
487    /// Version
488    pub version: u64,
489    /// Nonce
490    pub nonce: u64,
491    /// number
492    pub number: u64,
493    /// Minimal proof of work difficulty
494    pub pow_min: u64,
495    /// Local time of the block issuer
496    pub time: u64,
497    /// Average time
498    pub median_time: u64,
499    /// Members count
500    pub members_count: u64,
501    /// Monetary mass
502    pub monetary_mass: u64,
503    /// Unit base (power of ten)
504    #[serde(rename = "unitbase")]
505    pub unit_base: u64,
506    /// Number of compute members in the current frame
507    pub issuers_count: u64,
508    /// Current frame size (in blocks)
509    pub issuers_frame: u64,
510    /// Current frame variation buffer
511    pub issuers_frame_var: i64,
512    /// Currency.
513    pub currency: String,
514    /// Block issuer.
515    pub issuer: String,
516    /// Block signature.
517    pub signature: String,
518    /// Block hash.
519    pub hash: Option<String>,
520    /// Currency parameters (only in genesis block)
521    pub parameters: Option<String>,
522    /// Hash of the previous block
523    pub previous_hash: Option<String>,
524    /// Issuer of the previous block
525    pub previous_issuer: Option<String>,
526    /// Hash of the deterministic content of the block
527    #[serde(rename = "inner_hash")]
528    pub inner_hash: Option<String>,
529    /// Amount of new dividend created at this block, None if no dividend is created at this block
530    pub dividend: Option<u64>,
531    /// Identities
532    pub identities: Vec<String>,
533    /// joiners
534    pub joiners: Vec<String>,
535    /// Actives (=renewals)
536    pub actives: Vec<String>,
537    /// Leavers
538    pub leavers: Vec<String>,
539    /// Revokeds
540    pub revoked: Vec<String>,
541    /// Excludeds
542    pub excluded: Vec<String>,
543    /// Certifications
544    pub certifications: Vec<String>,
545    /// Transactions
546    pub transactions: Vec<TransactionDocumentV10Stringified>,
547}
548
549impl ToStringObject for DubpBlockV10 {
550    type StringObject = DubpBlockV10Stringified;
551    /// Transforms an object into a json object
552    fn to_string_object(&self) -> DubpBlockV10Stringified {
553        DubpBlockV10Stringified {
554            version: self.content.version as u64,
555            nonce: self.nonce,
556            number: u64::from(self.content.number.0),
557            pow_min: self.content.pow_min as u64,
558            time: self.content.time,
559            median_time: self.content.median_time,
560            members_count: self.content.members_count as u64,
561            monetary_mass: self.content.monetary_mass,
562            unit_base: self.content.unit_base as u64,
563            issuers_count: self.content.issuers_count as u64,
564            issuers_frame: self.content.issuers_frame as u64,
565            issuers_frame_var: self.content.issuers_frame_var as i64,
566            currency: self.content.currency.to_string(),
567            issuer: self.content.issuer.to_string(),
568            signature: self.signature.to_string(),
569            hash: Some(self.hash.to_string()),
570            parameters: self
571                .content
572                .parameters
573                .map(|parameters| parameters.to_string()),
574            previous_hash: if self.content.number.0 == 0 {
575                None
576            } else {
577                Some(self.content.previous_hash.to_string())
578            },
579            previous_issuer: if self.content.number.0 == 0 {
580                None
581            } else {
582                Some(self.content.previous_issuer.to_string())
583            },
584            inner_hash: self.inner_hash.map(|hash| hash.to_string()),
585            dividend: self.content.dividend.map(|d| d as u64),
586            identities: self
587                .content
588                .identities
589                .iter()
590                .map(|doc| doc.to_compact_document().as_compact_text())
591                .collect(),
592            joiners: self
593                .content
594                .joiners
595                .iter()
596                .map(|doc| doc.to_compact_document().as_compact_text())
597                .collect(),
598            actives: self
599                .content
600                .actives
601                .iter()
602                .map(|doc| doc.to_compact_document().as_compact_text())
603                .collect(),
604            leavers: self
605                .content
606                .leavers
607                .iter()
608                .map(|doc| doc.to_compact_document().as_compact_text())
609                .collect(),
610            revoked: self
611                .content
612                .revoked
613                .iter()
614                .map(|doc| doc.to_compact_document().as_compact_text())
615                .collect(),
616            excluded: self
617                .content
618                .excluded
619                .iter()
620                .map(ToString::to_string)
621                .collect(),
622            certifications: self
623                .content
624                .certifications
625                .iter()
626                .map(|doc| doc.to_compact_document().as_compact_text())
627                .collect(),
628            transactions: self
629                .content
630                .transactions
631                .iter()
632                .map(|tx_doc| tx_doc.to_string_object())
633                .collect(),
634        }
635    }
636}
637
638impl FromStringObject for DubpBlockV10 {
639    fn from_string_object(stringified: &DubpBlockV10Stringified) -> Result<Self, TextParseError> {
640        let str_identities: Vec<_> = stringified.identities.iter().map(|x| &**x).collect();
641        let str_joiners: Vec<_> = stringified.joiners.iter().map(|x| &**x).collect();
642        let str_actives: Vec<_> = stringified.actives.iter().map(|x| &**x).collect();
643        let str_leavers: Vec<_> = stringified.leavers.iter().map(|x| &**x).collect();
644        let str_revoked: Vec<_> = stringified.revoked.iter().map(|x| &**x).collect();
645        let str_certs: Vec<_> = stringified.certifications.iter().map(|x| &**x).collect();
646        Ok(DubpBlockV10 {
647            content: DubpBlockV10Content {
648                version: stringified.version as usize,
649                number: BlockNumber(stringified.number as u32),
650                pow_min: stringified.pow_min as usize,
651                time: stringified.time,
652                median_time: stringified.median_time,
653                members_count: stringified.members_count as usize,
654                monetary_mass: stringified.monetary_mass,
655                unit_base: stringified.unit_base as usize,
656                issuers_count: stringified.issuers_count as usize,
657                issuers_frame: stringified.issuers_frame as usize,
658                issuers_frame_var: stringified.issuers_frame_var as isize,
659                currency: CurrencyName(stringified.currency.clone()),
660                issuer: ed25519::PublicKey::from_base58(&stringified.issuer).map_err(|error| {
661                    TextParseError::BaseConversionError {
662                        field: "block.issuer",
663                        error,
664                    }
665                })?,
666                parameters: None,
667                previous_hash: if let Some(ref previous_hash) = stringified.previous_hash {
668                    if !previous_hash.is_empty() {
669                        Hash::from_hex(previous_hash).map_err(|error| {
670                            TextParseError::BaseConversionError {
671                                field: "block.previous_hash",
672                                error,
673                            }
674                        })?
675                    } else {
676                        Hash::default()
677                    }
678                } else {
679                    Hash::default()
680                },
681                previous_issuer: if let Some(ref previous_issuer) = stringified.previous_issuer {
682                    if !previous_issuer.is_empty() {
683                        ed25519::PublicKey::from_base58(previous_issuer).map_err(|error| {
684                            TextParseError::BaseConversionError {
685                                field: "block.previous_issuer",
686                                error,
687                            }
688                        })?
689                    } else {
690                        ed25519::PublicKey::default()
691                    }
692                } else {
693                    ed25519::PublicKey::default()
694                },
695                dividend: stringified.dividend.map(|dividend| dividend as usize),
696                identities: parse_compact_identities(&stringified.currency, &str_identities)
697                    .map_err(|error| TextParseError::CompactDoc {
698                        field: "block.identities",
699                        error,
700                    })?,
701                joiners: parse_compact_memberships(
702                    &stringified.currency,
703                    MembershipType::In(),
704                    &str_joiners,
705                )
706                .map_err(|error| TextParseError::CompactDoc {
707                    field: "block.joiners",
708                    error,
709                })?,
710                actives: parse_compact_memberships(
711                    &stringified.currency,
712                    MembershipType::In(),
713                    &str_actives,
714                )
715                .map_err(|error| TextParseError::CompactDoc {
716                    field: "block.actives",
717                    error,
718                })?,
719                leavers: parse_compact_memberships(
720                    &stringified.currency,
721                    MembershipType::Out(),
722                    &str_leavers,
723                )
724                .map_err(|error| TextParseError::CompactDoc {
725                    field: "block.leavers",
726                    error,
727                })?,
728                revoked: parse_compact_revocations(&str_revoked).map_err(|error| {
729                    TextParseError::CompactDoc {
730                        field: "block.revoked",
731                        error,
732                    }
733                })?,
734                excluded: stringified
735                    .excluded
736                    .iter()
737                    .map(|pubkey| ed25519::PublicKey::from_base58(pubkey))
738                    .collect::<Result<Vec<_>, _>>()
739                    .map_err(|error| TextParseError::BaseConversionError {
740                        field: "block.excluded",
741                        error,
742                    })?,
743                certifications: parse_compact_certifications(&str_certs).map_err(|error| {
744                    TextParseError::CompactDoc {
745                        field: "block.certifications",
746                        error,
747                    }
748                })?,
749                transactions: stringified
750                    .transactions
751                    .iter()
752                    .map(|tx| TransactionDocumentV10::from_string_object(tx))
753                    .collect::<Result<Vec<_>, _>>()?,
754            },
755            inner_hash: Some(
756                Hash::from_hex(stringified.inner_hash.as_ref().ok_or_else(|| {
757                    TextParseError::InvalidInnerFormat("Block without inner_hash".to_owned())
758                })?)
759                .map_err(|error| TextParseError::BaseConversionError {
760                    field: "block.inner_hash",
761                    error,
762                })?,
763            ),
764            nonce: stringified.nonce,
765            signature: ed25519::Signature::from_base64(&stringified.signature).map_err(
766                |error| TextParseError::BaseConversionError {
767                    field: "block.signature",
768                    error,
769                },
770            )?,
771            hash: BlockHash(
772                Hash::from_hex(stringified.hash.as_ref().ok_or_else(|| {
773                    TextParseError::InvalidInnerFormat("Block without hash".to_owned())
774                })?)
775                .map_err(|error| TextParseError::BaseConversionError {
776                    field: "block.hash",
777                    error,
778                })?,
779            ),
780        })
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use crate::tests::*;
788    use dubp_documents::certification::CertificationDocument;
789    use dubp_documents::membership::MembershipDocument;
790    use dubp_documents::transaction::TransactionDocument;
791    use pretty_assertions::assert_eq;
792    use unwrap::unwrap;
793
794    #[test]
795    fn test_default_block_v10() {
796        let mut default_block = DubpBlockV10::default();
797
798        let mut default_stringified_block = default_block.to_string_object();
799        default_stringified_block.currency = String::with_capacity(0);
800        default_stringified_block.issuer = String::with_capacity(0);
801        default_stringified_block.signature = String::with_capacity(0);
802        default_stringified_block.hash = None;
803        assert_eq!(
804            default_stringified_block,
805            DubpBlockV10Stringified::default()
806        );
807
808        assert_eq!(default_block.common_time(), 0);
809        assert_eq!(default_block.current_frame_size(), 0);
810        assert_eq!(default_block.issuers_count(), 0);
811        assert_eq!(default_block.members_count(), 0);
812        assert_eq!(default_block.number(), BlockNumber(0));
813        assert_eq!(default_block.pow_min(), 0);
814        assert_eq!(default_block.previous_blockstamp(), Blockstamp::default());
815        assert_eq!(default_block.previous_hash(), Hash::default());
816
817        // Inner hash
818        assert_eq!(
819            Err(VerifyBlockHashError::MissingHash {
820                block_number: BlockNumber(0)
821            }),
822            default_block.verify_inner_hash()
823        );
824        default_block.inner_hash = Some(Hash::default());
825        assert_eq!(
826            Err(VerifyBlockHashError::InvalidHash {
827                block_number: BlockNumber(0),
828                actual_hash: Hash::default(),
829                expected_hash: default_block.compute_inner_hash(),
830            }),
831            default_block.verify_inner_hash()
832        );
833
834        // Signature
835        assert_eq!(default_block.signature, ed25519::Signature::default());
836        assert_eq!(Err(SigError::InvalidSig), default_block.verify_signature());
837        let signator = unwrap!(ed25519::Ed25519KeyPair::generate_random()).generate_signator();
838        default_block.content.issuer = signator.public_key();
839        unwrap!(default_block.sign(&signator));
840        assert_eq!(Ok(()), default_block.verify_signature());
841
842        // Hash
843        assert_eq!(BlockHash(Hash::default()), default_block.hash());
844        assert_eq!(
845            Err(VerifyBlockHashError::InvalidHash {
846                block_number: BlockNumber(0),
847                actual_hash: Hash::default(),
848                expected_hash: default_block.compute_hash().0,
849            }),
850            default_block.verify_hash()
851        );
852    }
853
854    #[test]
855    fn generate_and_verify_empty_block() {
856        let block_content = DubpBlockV10Content {
857            version: 10,
858            number: BlockNumber(174_260),
859            pow_min: 68,
860            time: 1_525_296_873,
861            median_time: 1_525_292_577,
862            members_count: 33,
863            monetary_mass: 15_633_687,
864            unit_base: 0,
865            issuers_count: 8,
866            issuers_frame: 41,
867            issuers_frame_var: 0,
868            currency: CurrencyName(String::from("g1-test")),
869            issuer: pk("39Fnossy1GrndwCnAXGDw3K5UYXhNXAFQe7yhYZp8ELP"),
870            parameters: None,
871            previous_hash: unwrap!(Hash::from_hex(
872                "0000A7D4361B9EBF4CE974A521149A73E8A5DE9B73907AB3BC918726AED7D40A"
873            )),
874            previous_issuer: pk("EPKuZA1Ek5y8S1AjAmAPtGrVCMFqUGzUEAa7Ei62CY2L"),
875            dividend: None,
876            identities: Vec::new(),
877            joiners: Vec::new(),
878            actives: Vec::new(),
879            leavers: Vec::new(),
880            revoked: Vec::new(),
881            excluded: Vec::new(),
882            certifications: Vec::new(),
883            transactions: Vec::new(),
884        };
885        let mut block = DubpBlockV10Builder::new(block_content).build_unchecked(DubpBlockV10AfterPowData {
886            nonce: 100_010_200_000_006_940,
887            signature: unwrap!(ed25519::Signature::from_base64("lqXrNOopjM39oM7hgB7Vq13uIohdCuLlhh/q8RVVEZ5UVASphow/GXikCdhbWID19Bn0XrXzTbt/R7akbE9xAg==")),
888            hash: BlockHash::default(),
889        });
890        // test inner_hash computation
891        println!("{}", block.generate_compact_inner_text());
892        assert!(block.verify_inner_hash().is_ok());
893        assert_eq!(
894            unwrap!(block.inner_hash).to_hex(),
895            "58E4865A47A46E0DF1449AABC449B5406A12047C413D61B5E17F86BE6641E7B0"
896        );
897        // Test signature validity
898        assert!(block.verify_signature().is_ok());
899        // Test hash computation
900        let computed_hash = block.compute_hash();
901        block.hash = computed_hash;
902        assert!(block.verify_hash().is_ok());
903        assert_eq!(
904            block.hash.0.to_hex(),
905            "00002EE584F36C15D3EB21AAC78E0896C75EF9070E73B4EC33BFA2C3D561EEB2"
906        );
907    }
908
909    #[test]
910    fn generate_and_verify_block() {
911        let cert1 = unwrap!(CertificationDocument::parse_from_raw_text("Version: 10
912Type: Certification
913Currency: g1
914Issuer: 6TAzLWuNcSqgNDNpAutrKpPXcGJwy1ZEMeVvZSZNs2e3
915IdtyIssuer: CYPsYTdt87Tx6cCiZs9KD4jqPgYxbcVEqVZpRgJ9jjoV
916IdtyUniqueID: PascaleM
917IdtyTimestamp: 97401-0000003821911909F98519CC773D2D3E5CFE3D5DBB39F4F4FF33B96B4D41800E
918IdtySignature: QncUVXxZ2NfARjdJOn6luILvDuG1NuK9qSoaU4CST2Ij8z7oeVtEgryHl+EXOjSe6XniALsCT0gU8wtadcA/Cw==
919CertTimestamp: 106669-000003682E6FE38C44433DCE92E8B2A26C69B6D7867A2BAED231E788DDEF4251
920UmseG2XKNwKcY8RFi6gUCT91udGnnNmSh7se10J1jeRVlwf+O2Tyb2Cccot9Dt7BO4+Kx2P6vFJB3oVGGHMxBA=="));
921        let CertificationDocument::V10(cert1) = cert1;
922
923        let TransactionDocument::V10(tx1) = unwrap!(TransactionDocument::parse_from_raw_text("Version: 10
924Type: Transaction
925Currency: g1
926Blockstamp: 107982-000001242F6DA51C06A915A96C58BAA37AB3D1EB51F6E1C630C707845ACF764B
927Locktime: 0
928Issuers:
9298dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3
930Inputs:
9311002:0:D:8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3:106345
932Unlocks:
9330:SIG(0)
934Outputs:
9351002:0:SIG(CitdnuQgZ45tNFCagay7Wh12gwwHM8VLej1sWmfHWnQX)
936Comment: DU symbolique pour demander le codage de nouvelles fonctionnalites cf. https://forum.monnaie-libre.fr/t/creer-de-nouvelles-fonctionnalites-dans-cesium-les-autres-applications/2025  Merci
937T0LlCcbIn7xDFws48H8LboN6NxxwNXXTovG4PROLf7tkUAueHFWjfwZFKQXeZEHxfaL1eYs3QspGtLWUHPRVCQ=="));
938
939        let TransactionDocument::V10(tx2) = unwrap!(TransactionDocument::parse_from_raw_text("Version: 10
940Type: Transaction
941Currency: g1
942Blockstamp: 107982-000001242F6DA51C06A915A96C58BAA37AB3D1EB51F6E1C630C707845ACF764B
943Locktime: 0
944Issuers:
9458dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3
946Inputs:
9471002:0:D:8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3:106614
948Unlocks:
9490:SIG(0)
950Outputs:
9511002:0:SIG(78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8)
952Comment: DU symbolique pour demander le codage de nouvelles fonctionnalites cf. https://forum.monnaie-libre.fr/t/creer-de-nouvelles-fonctionnalites-dans-cesium-les-autres-applications/2025  Merci
953a9PHPuSfw7jW8FRQHXFsGi/bnLjbtDnTYvEVgUC9u0WlR7GVofa+Xb+l5iy6NwuEXiwvueAkf08wPVY8xrNcCg=="));
954
955        let block_content = DubpBlockV10Content {
956            version: 10,
957            number: BlockNumber(107_984),
958            pow_min: 88,
959            time: 1_522_685_861,
960            median_time: 1_522_683_184,
961            members_count: 896,
962            monetary_mass: 140_469_765,
963            unit_base: 0,
964            issuers_count: 42,
965            issuers_frame: 211,
966            issuers_frame_var: 0,
967            currency: CurrencyName(String::from("g1")),
968            issuer: pk("DA4PYtXdvQqk1nCaprXH52iMsK5Ahxs1nRWbWKLhpVkQ"),
969            parameters: None,
970            previous_hash: unwrap!(Hash::from_hex(
971                "000001144968D0C3516BE6225E4662F182E28956AF46DD7FB228E3D0F9413FEB"
972            )),
973            previous_issuer: pk("D3krfq6J9AmfpKnS3gQVYoy7NzGCc61vokteTS8LJ4YH"),
974            dividend: None,
975            identities: Vec::new(),
976            joiners: Vec::new(),
977            actives: Vec::new(),
978            leavers: Vec::new(),
979            revoked: Vec::new(),
980            excluded: Vec::new(),
981            certifications: vec![TextDocumentFormat::Complete(cert1)],
982            transactions: vec![tx1, tx2],
983        };
984        let mut block = DubpBlockV10Builder::new(block_content).build_unchecked(DubpBlockV10AfterPowData {
985            nonce: 10_300_000_018_323,
986            signature: unwrap!(ed25519::Signature::from_base64("92id58VmkhgVNee4LDqBGSm8u/ooHzAD67JM6fhAE/CV8LCz7XrMF1DvRl+eRpmlaVkp6I+Iy8gmZ1WUM5C8BA==")),
987            hash: BlockHash::default(),
988        });
989        // test inner_hash computation
990        println!("{}", block.generate_compact_inner_text());
991        assert!(block.verify_inner_hash().is_ok());
992
993        assert_eq!(
994            unwrap!(block.inner_hash).to_hex(),
995            "C8AB69E33ECE2612EADC7AB30D069B1F1A3D8C95EBBFD50DE583AC8E3666CCA1"
996        );
997        // test generate_compact_inner_text()
998        assert_eq!(
999            block.generate_compact_inner_text(),
1000            "Version: 10\nType: Block\nCurrency: g1\nNumber: 107984\nPoWMin: 88\nTime: 1522685861\nMedianTime: 1522683184\nUnitBase: 0\nIssuer: DA4PYtXdvQqk1nCaprXH52iMsK5Ahxs1nRWbWKLhpVkQ\nIssuersFrame: 211\nIssuersFrameVar: 0\nDifferentIssuersCount: 42\nPreviousHash: 000001144968D0C3516BE6225E4662F182E28956AF46DD7FB228E3D0F9413FEB\nPreviousIssuer: D3krfq6J9AmfpKnS3gQVYoy7NzGCc61vokteTS8LJ4YH\nMembersCount: 896\nIdentities:\nJoiners:\nActives:\nLeavers:\nRevoked:\nExcluded:\nCertifications:\n6TAzLWuNcSqgNDNpAutrKpPXcGJwy1ZEMeVvZSZNs2e3:CYPsYTdt87Tx6cCiZs9KD4jqPgYxbcVEqVZpRgJ9jjoV:106669:UmseG2XKNwKcY8RFi6gUCT91udGnnNmSh7se10J1jeRVlwf+O2Tyb2Cccot9Dt7BO4+Kx2P6vFJB3oVGGHMxBA==\nTransactions:\nTX:10:1:1:1:1:1:0\n107982-000001242F6DA51C06A915A96C58BAA37AB3D1EB51F6E1C630C707845ACF764B\n8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3\n1002:0:D:8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3:106345\n0:SIG(0)\n1002:0:SIG(CitdnuQgZ45tNFCagay7Wh12gwwHM8VLej1sWmfHWnQX)\nDU symbolique pour demander le codage de nouvelles fonctionnalites cf. https://forum.monnaie-libre.fr/t/creer-de-nouvelles-fonctionnalites-dans-cesium-les-autres-applications/2025  Merci\nT0LlCcbIn7xDFws48H8LboN6NxxwNXXTovG4PROLf7tkUAueHFWjfwZFKQXeZEHxfaL1eYs3QspGtLWUHPRVCQ==\nTX:10:1:1:1:1:1:0\n107982-000001242F6DA51C06A915A96C58BAA37AB3D1EB51F6E1C630C707845ACF764B\n8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3\n1002:0:D:8dkCwvAqSczUjKsoVMDPVbQ3i6bBQeBQYawL87kqTSQ3:106614\n0:SIG(0)\n1002:0:SIG(78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8)\nDU symbolique pour demander le codage de nouvelles fonctionnalites cf. https://forum.monnaie-libre.fr/t/creer-de-nouvelles-fonctionnalites-dans-cesium-les-autres-applications/2025  Merci\na9PHPuSfw7jW8FRQHXFsGi/bnLjbtDnTYvEVgUC9u0WlR7GVofa+Xb+l5iy6NwuEXiwvueAkf08wPVY8xrNcCg==\n"
1001        );
1002        // Test signature validity
1003        assert!(block.verify_signature().is_ok());
1004        // Test hash computation
1005        block.hash = block.compute_hash();
1006        assert!(block.verify_hash().is_ok());
1007        assert_eq!(
1008            block.hash.0.to_hex(),
1009            "000004F8B84A3590243BA562E5F2BA379F55A0B387C5D6FAC1022DFF7FFE6014"
1010        );
1011
1012        // Test reduce factor
1013        let block_size = unwrap!(bincode::serialize(&block)).len();
1014        block.reduce();
1015        let block_reduced_size = unwrap!(bincode::serialize(&block)).len();
1016        assert!(block_reduced_size < block_size);
1017        println!(
1018            "block reduction: {} octets -> {} octets",
1019            block_size, block_reduced_size
1020        );
1021    }
1022
1023    #[test]
1024    fn generate_and_verify_block_2() {
1025        let ms1 = unwrap!(MembershipDocument::parse_from_raw_text(
1026            "Version: 10
1027Type: Membership
1028Currency: g1
1029Issuer: 4VZkro3N7VonygybESHngKUABA6gSrbW77Ktb94zE969
1030Block: 165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2
1031Membership: IN
1032UserID: piaaf31
1033CertTS: 74077-0000022816648B2F7801E059F67CCD0C023FF0ED84459D52C70494D74DDCC6F6
1034gvaZ1QnJf8FjjRDJ0cYusgpBgQ8r0NqEz39BooH6DtIrgX+WTeXuLSnjZDl35VCBjokvyjry+v0OkTT8FKpABA==",
1035        ));
1036        let MembershipDocument::V10(ms1) = ms1;
1037
1038        let TransactionDocument::V10(tx1) = unwrap!(TransactionDocument::parse_from_raw_text(
1039            "Version: 10
1040Type: Transaction
1041Currency: g1
1042Blockstamp: 165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2
1043Locktime: 0
1044Issuers:
104551EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2
1046Inputs:
10471004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:163766
10481004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164040
10491004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164320
10501004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164584
10511004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164849
10521004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:165118
10531004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:165389
1054Unlocks:
10550:SIG(0)
10561:SIG(0)
10572:SIG(0)
10583:SIG(0)
10594:SIG(0)
10605:SIG(0)
10616:SIG(0)
1062Outputs:
10637000:0:SIG(98wxzS683Tc1WWm1YxpL5WpxS7wBa1mZBccKSsYpaant)
106428:0:SIG(51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2)
1065Comment: Panier mixte plus 40 pommes merci
10667o/yIh0BNSAv5pNmHz04uUBl8TuP2s4HRFMtKeGFQfXNYJPUyJTP/dj6hdrgKtJkm5dCfbxT4KRy6wJf+dj1Cw==",
1067        ));
1068
1069        let TransactionDocument::V10(tx2) = unwrap!(TransactionDocument::parse_from_raw_text(
1070            "Version: 10
1071Type: Transaction
1072Currency: g1
1073Blockstamp: 165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2
1074Locktime: 0
1075Issuers:
10763Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX
1077Inputs:
10781002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:148827
10791002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149100
10801002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149370
10811002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149664
10821002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149943
10831002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:150222
1084Unlocks:
10850:SIG(0)
10861:SIG(0)
10872:SIG(0)
10883:SIG(0)
10894:SIG(0)
10905:SIG(0)
1091Outputs:
10926000:0:SIG(AopwTfXhj8VqZReFJYGGWnoWnXNj3RgaqFcGGywXpZrD)
109312:0:SIG(3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX)
1094Comment: En reglement de tes bons bocaux de fruits et legumes
1095nxr4exGrt16jteN9ZX3XZPP9l+X0OUbZ1o/QjE1hbWQNtVU3HhH9SJoEvNj2iVl3gCRr9u2OA9uj9vCyUDyjAg==
1096",
1097        ));
1098
1099        let block_content = DubpBlockV10Content {
1100            version: 10,
1101            number: BlockNumber(165_647),
1102            pow_min: 90,
1103            time: 1_540_633_175,
1104            median_time: 1_540_627_811,
1105            members_count: 1402,
1106            monetary_mass: 386_008_811,
1107            unit_base: 0,
1108            issuers_count: 37,
1109            issuers_frame: 186,
1110            issuers_frame_var: 0,
1111            currency: CurrencyName(String::from("g1")),
1112            issuer: pk("A4pc9Uuk4NXkWG8CibicjjPpEPdiup1mhjMoRWUZsonq"),
1113            parameters: None,
1114            previous_hash: unwrap!(Hash::from_hex(
1115                "000003E78FA4133F2C13B416F330C8DFB5A41EB87E37190615DB334F2C914A51"
1116            )),
1117            previous_issuer: pk("8NmGZmGjL1LUgJQRg282yQF7KTdQuRNAg8QfSa2qvd65"),
1118            dividend: None,
1119            identities: vec![],
1120            joiners: vec![],
1121            actives: vec![ms1],
1122            leavers: vec![],
1123            revoked: vec![],
1124            excluded: vec![],
1125            certifications: vec![],
1126            transactions: vec![tx1, tx2],
1127        };
1128        let mut block = DubpBlockV10Builder::new(block_content).build_unchecked(DubpBlockV10AfterPowData {
1129            nonce: 10_300_000_090_296,
1130            signature: unwrap!(ed25519::Signature::from_base64("2Z/+9ADdZvHXs19YR8+qDzgfl8WJlBG5PcbFvBG9TOuUJbjAdxhcgxrFrSRIABGWcCrIgLkB805fZVLP8jOjBA==")),
1131            hash: BlockHash::default(),
1132        });
1133        // test inner_hash computation
1134        println!("{}", block.generate_compact_inner_text());
1135        assert!(block.verify_inner_hash().is_ok());
1136        assert_eq!(
1137            unwrap!(block.inner_hash).to_hex(),
1138            "3B49ECC1475549CFD94CA7B399311548A0FD0EC93C8EDD5670DAA5A958A41846"
1139        );
1140        // test generate_compact_inner_text()
1141        let block_compact_text = block.generate_compact_inner_text();
1142        assert_eq!(
1143            block_compact_text,
1144            "Version: 10\nType: Block\nCurrency: g1\nNumber: 165647\nPoWMin: 90\nTime: 1540633175\nMedianTime: 1540627811\nUnitBase: 0\nIssuer: A4pc9Uuk4NXkWG8CibicjjPpEPdiup1mhjMoRWUZsonq\nIssuersFrame: 186\nIssuersFrameVar: 0\nDifferentIssuersCount: 37\nPreviousHash: 000003E78FA4133F2C13B416F330C8DFB5A41EB87E37190615DB334F2C914A51\nPreviousIssuer: 8NmGZmGjL1LUgJQRg282yQF7KTdQuRNAg8QfSa2qvd65\nMembersCount: 1402\nIdentities:\nJoiners:\nActives:\n4VZkro3N7VonygybESHngKUABA6gSrbW77Ktb94zE969:gvaZ1QnJf8FjjRDJ0cYusgpBgQ8r0NqEz39BooH6DtIrgX+WTeXuLSnjZDl35VCBjokvyjry+v0OkTT8FKpABA==:165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2:74077-0000022816648B2F7801E059F67CCD0C023FF0ED84459D52C70494D74DDCC6F6:piaaf31\nLeavers:\nRevoked:\nExcluded:\nCertifications:\nTransactions:\nTX:10:1:7:7:2:1:0\n165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2\n51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:163766\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164040\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164320\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164584\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:164849\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:165118\n1004:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:165389\n0:SIG(0)\n1:SIG(0)\n2:SIG(0)\n3:SIG(0)\n4:SIG(0)\n5:SIG(0)\n6:SIG(0)\n7000:0:SIG(98wxzS683Tc1WWm1YxpL5WpxS7wBa1mZBccKSsYpaant)\n28:0:SIG(51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2)\nPanier mixte plus 40 pommes merci\n7o/yIh0BNSAv5pNmHz04uUBl8TuP2s4HRFMtKeGFQfXNYJPUyJTP/dj6hdrgKtJkm5dCfbxT4KRy6wJf+dj1Cw==\nTX:10:1:6:6:2:1:0\n165645-000002D30130881939961A38D51CA233B3C696AA604439036DB1AAA4ED5046D2\n3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:148827\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149100\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149370\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149664\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:149943\n1002:0:D:3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX:150222\n0:SIG(0)\n1:SIG(0)\n2:SIG(0)\n3:SIG(0)\n4:SIG(0)\n5:SIG(0)\n6000:0:SIG(AopwTfXhj8VqZReFJYGGWnoWnXNj3RgaqFcGGywXpZrD)\n12:0:SIG(3Uwq4qNp2A97P1XQueEBCxmnvgtAKMdfrEq6VB7Ph2qX)\nEn reglement de tes bons bocaux de fruits et legumes\nnxr4exGrt16jteN9ZX3XZPP9l+X0OUbZ1o/QjE1hbWQNtVU3HhH9SJoEvNj2iVl3gCRr9u2OA9uj9vCyUDyjAg==\n"
1145        );
1146        // Test signature validity
1147        assert!(block.verify_signature().is_ok());
1148        // Test hash computation
1149        block.hash = block.compute_hash();
1150        assert!(block.verify_hash().is_ok());
1151        assert_eq!(
1152            block.hash.0.to_hex(),
1153            "000002026E32A3D649B34968AAF9D03C4F19A5954229C54A801BBB1CD216B230"
1154        );
1155    }
1156}