block.py 18.3 KB
Newer Older
1 2 3
import base64
import hashlib
import re
4 5
from typing import TypeVar, Type, Optional, List, Sequence
from .block_uid import BlockUID
6 7
from .certification import Certification
from .revocation import Revocation
8
from .identity import Identity
9
from .document import Document, MalformedDocumentError
inso's avatar
inso committed
10 11
from .membership import Membership
from .transaction import Transaction
12
from ..constants import PUBKEY_REGEX, BLOCK_HASH_REGEX
13 14 15 16 17 18


# required to type hint cls in classmethod
BlockType = TypeVar('BlockType', bound='Block')


inso's avatar
inso committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
class Block(Document):
    """
The class Block handles Block documents.

.. note:: A block document is specified by the following format :

    | Version: VERSION
    | Type: Block
    | Currency: CURRENCY
    | Nonce: NONCE
    | Number: BLOCK_NUMBER
    | PoWMin: NUMBER_OF_ZEROS
    | Time: GENERATED_ON
    | MedianTime: MEDIAN_DATE
    | UniversalDividend: DIVIDEND_AMOUNT
    | Issuer: ISSUER_KEY
    | PreviousHash: PREVIOUS_HASH
    | PreviousIssuer: PREVIOUS_ISSUER_KEY
    | Parameters: PARAMETERS
    | MembersCount: WOT_MEM_COUNT
    | Identities:
    | PUBLIC_KEY:SIGNATURE:TIMESTAMP:USER_ID
    | ...
    | Joiners:
    | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
    | ...
    | Actives:
    | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
    | ...
    | Leavers:
    | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
    | ...
    | Excluded:
    | PUBLIC_KEY
    | ...
    | Certifications:
    | PUBKEY_FROM:PUBKEY_TO:BLOCK_NUMBER:SIGNATURE
    | ...
    | Transactions:
    | COMPACT_TRANSACTION
    | ...
    | BOTTOM_SIGNATURE

    """

    re_type = re.compile("Type: (Block)\n")
    re_number = re.compile("Number: ([0-9]+)\n")
    re_powmin = re.compile("PoWMin: ([0-9]+)\n")
    re_time = re.compile("Time: ([0-9]+)\n")
    re_mediantime = re.compile("MedianTime: ([0-9]+)\n")
    re_universaldividend = re.compile("UniversalDividend: ([0-9]+)\n")
inso's avatar
inso committed
70
    re_unitbase = re.compile("UnitBase: ([0-9]+)\n")
71
    re_issuer = re.compile("Issuer: ({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
inso's avatar
inso committed
72
    re_issuers_frame = re.compile("IssuersFrame: ([0-9]+)\n")
73
    re_issuers_frame_var = re.compile("IssuersFrameVar: (0|-?[1-9]\\d{0,18})\n")
inso's avatar
inso committed
74
    re_different_issuers_count = re.compile("DifferentIssuersCount: ([0-9]+)\n")
75 76
    re_previoushash = re.compile("PreviousHash: ({block_hash_regex})\n".format(block_hash_regex=BLOCK_HASH_REGEX))
    re_previousissuer = re.compile("PreviousIssuer: ({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
77 78
    re_parameters = re.compile("Parameters: ([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):\
([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):\
79
([0-9]+):([0-9]+):([0-9]+)\n")
inso's avatar
inso committed
80 81 82 83 84 85 86
    re_memberscount = re.compile("MembersCount: ([0-9]+)\n")
    re_identities = re.compile("Identities:\n")
    re_joiners = re.compile("Joiners:\n")
    re_actives = re.compile("Actives:\n")
    re_leavers = re.compile("Leavers:\n")
    re_revoked = re.compile("Revoked:\n")
    re_excluded = re.compile("Excluded:\n")
87
    re_exclusion = re.compile("({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
inso's avatar
inso committed
88 89
    re_certifications = re.compile("Certifications:\n")
    re_transactions = re.compile("Transactions:\n")
90
    re_hash = re.compile("InnerHash: ({block_hash_regex})\n".format(block_hash_regex=BLOCK_HASH_REGEX))
inso's avatar
inso committed
91 92 93
    re_noonce = re.compile("Nonce: ([0-9]+)\n")

    fields_parsers = {**Document.fields_parsers, **{
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
        'Type': re_type,
        'Number': re_number,
        'PoWMin': re_powmin,
        'Time': re_time,
        'MedianTime': re_mediantime,
        'UD': re_universaldividend,
        'UnitBase': re_unitbase,
        'Issuer': re_issuer,
        'IssuersFrame': re_issuers_frame,
        'IssuersFrameVar': re_issuers_frame_var,
        'DifferentIssuersCount': re_different_issuers_count,
        'PreviousIssuer': re_previousissuer,
        'PreviousHash': re_previoushash,
        'Parameters': re_parameters,
        'MembersCount': re_memberscount,
        'Identities': re_identities,
        'Joiners': re_joiners,
        'Actives': re_actives,
        'Leavers': re_leavers,
        'Revoked': re_revoked,
        'Excluded': re_excluded,
        'Certifications': re_certifications,
        'Transactions': re_transactions,
        'InnerHash': re_hash,
        'Noonce': re_noonce,
    }
120
                      }
inso's avatar
inso committed
121

122 123 124 125 126 127 128 129 130 131 132 133 134
    def __init__(self,
                 version: int,
                 currency: str,
                 number: int,
                 powmin: int,
                 time: int,
                 mediantime: int,
                 ud: Optional[int],
                 unit_base: int,
                 issuer: str,
                 issuers_frame: int,
                 issuers_frame_var: int,
                 different_issuers_count: int,
135 136 137
                 prev_hash: Optional[str],
                 prev_issuer: Optional[str],
                 parameters: Optional[Sequence[str]],
138 139 140 141 142 143 144 145 146 147 148 149 150
                 members_count: int,
                 identities: List[Identity],
                 joiners: List[Membership],
                 actives: List[Membership],
                 leavers: List[Membership],
                 revokations: List[Revocation],
                 excluded: List[str],
                 certifications: List[Certification],
                 transactions: List[Transaction],
                 inner_hash: str,
                 noonce: int,
                 signature: str
                 ) -> None:
inso's avatar
inso committed
151 152 153
        """
        Constructor

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
        :param version: duniter protocol version
        :param currency: the block currency
        :param number: the number of the block
        :param powmin: the powmin value of this block
        :param time: the timestamp of this block
        :param mediantime: the timestamp of the median time of this block
        :param ud: the dividend amount, or None if no dividend present in this block
        :param unit_base: the unit_base of the dividend, or None if no dividend present in this block
        :param issuer: the pubkey of the issuer of the block
        :param issuers_frame:
        :param issuers_frame_var:
        :param different_issuers_count: the count of issuers
        :param prev_hash: the previous block hash
        :param prev_issuer: the previous block issuer
        :param parameters: the parameters of the currency. Should only be present in block 0.
        :param members_count: the number of members found in this block
        :param identities: the self certifications declared in this block
        :param joiners: the joiners memberships via "IN" documents
        :param actives: renewed memberships via "IN" documents
        :param leavers: the leavers memberships via "OUT" documents
        :param revokations: revokations
        :param excluded: members excluded because of missing certifications
        :param certifications: certifications documents
        :param transactions: transactions documents
        :param inner_hash: the block hah
        :param noonce: the noonce value of the block
        :param signature: the block signature
inso's avatar
inso committed
181 182
        """
        super().__init__(version, currency, [signature])
inso's avatar
inso committed
183
        documents_versions = max(max([1] + [i.version for i in identities]),
184 185 186 187
                                 max([1] + [m.version for m in actives + leavers + joiners]),
                                 max([1] + [r.version for r in revokations]),
                                 max([1] + [c.version for c in certifications]),
                                 max([1] + [t.version for t in transactions]))
inso's avatar
inso committed
188
        if self.version < documents_versions:
189 190
            raise MalformedDocumentError(
                "Block version is too low : {0} < {1}".format(self.version, documents_versions))
inso's avatar
inso committed
191 192 193 194 195 196 197
        self.number = number
        self.powmin = powmin
        self.time = time
        self.mediantime = mediantime
        self.ud = ud
        self.unit_base = unit_base
        self.issuer = issuer
inso's avatar
inso committed
198 199 200
        self.issuers_frame = issuers_frame
        self.issuers_frame_var = issuers_frame_var
        self.different_issuers_count = different_issuers_count
inso's avatar
inso committed
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
        self.prev_hash = prev_hash
        self.prev_issuer = prev_issuer
        self.parameters = parameters
        self.members_count = members_count
        self.identities = identities
        self.joiners = joiners
        self.actives = actives
        self.leavers = leavers
        self.revoked = revokations
        self.excluded = excluded
        self.certifications = certifications
        self.transactions = transactions
        self.inner_hash = inner_hash
        self.noonce = noonce

    @property
217
    def blockUID(self) -> BlockUID:
inso's avatar
inso committed
218
        return BlockUID(self.number, self.proof_of_work())
219

inso's avatar
inso committed
220
    @classmethod
221
    def from_signed_raw(cls: Type[BlockType], signed_raw: str) -> BlockType:
inso's avatar
inso committed
222
        lines = signed_raw.splitlines(True)
inso's avatar
inso committed
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
        n = 0

        version = int(Block.parse_field("Version", lines[n]))
        n += 1

        Block.parse_field("Type", lines[n])
        n += 1

        currency = Block.parse_field("Currency", lines[n])
        n += 1

        number = int(Block.parse_field("Number", lines[n]))
        n += 1

        powmin = int(Block.parse_field("PoWMin", lines[n]))
        n += 1

        time = int(Block.parse_field("Time", lines[n]))
        n += 1

        mediantime = int(Block.parse_field("MedianTime", lines[n]))
        n += 1

246 247 248 249
        ud_match = Block.re_universaldividend.match(lines[n])
        ud = None
        unit_base = 0
        if ud_match is not None:
inso's avatar
inso committed
250 251 252
            ud = int(Block.parse_field("UD", lines[n]))
            n += 1

253 254
        unit_base = int(Block.parse_field("UnitBase", lines[n]))
        n += 1
inso's avatar
inso committed
255 256 257 258

        issuer = Block.parse_field("Issuer", lines[n])
        n += 1

259 260 261 262 263 264
        issuers_frame = Block.parse_field("IssuersFrame", lines[n])
        n += 1
        issuers_frame_var = Block.parse_field("IssuersFrameVar", lines[n])
        n += 1
        different_issuers_count = Block.parse_field("DifferentIssuersCount", lines[n])
        n += 1
inso's avatar
inso committed
265

inso's avatar
inso committed
266 267 268
        prev_hash = None
        prev_issuer = None
        if number > 0:
269
            prev_hash = str(Block.parse_field("PreviousHash", lines[n]))
inso's avatar
inso committed
270 271
            n += 1

272
            prev_issuer = str(Block.parse_field("PreviousIssuer", lines[n]))
inso's avatar
inso committed
273 274 275 276 277
            n += 1

        parameters = None
        if number == 0:
            try:
278 279 280 281
                params_match = Block.re_parameters.match(lines[n])
                if params_match is None:
                    raise MalformedDocumentError("Parameters")
                parameters = params_match.groups()
inso's avatar
inso committed
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
                n += 1
            except AttributeError:
                raise MalformedDocumentError("Parameters")

        members_count = int(Block.parse_field("MembersCount", lines[n]))
        n += 1

        identities = []
        joiners = []
        actives = []
        leavers = []
        revoked = []
        excluded = []
        certifications = []
        transactions = []

        if Block.re_identities.match(lines[n]) is not None:
            n += 1
            while Block.re_joiners.match(lines[n]) is None:
301
                selfcert = Identity.from_inline(version, currency, lines[n])
inso's avatar
inso committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
                identities.append(selfcert)
                n += 1

        if Block.re_joiners.match(lines[n]):
            n += 1
            while Block.re_actives.match(lines[n]) is None:
                membership = Membership.from_inline(version, currency, "IN", lines[n])
                joiners.append(membership)
                n += 1

        if Block.re_actives.match(lines[n]):
            n += 1
            while Block.re_leavers.match(lines[n]) is None:
                membership = Membership.from_inline(version, currency, "IN", lines[n])
                actives.append(membership)
                n += 1

        if Block.re_leavers.match(lines[n]):
            n += 1
            while Block.re_revoked.match(lines[n]) is None:
                membership = Membership.from_inline(version, currency, "OUT", lines[n])
                leavers.append(membership)
                n += 1

        if Block.re_revoked.match(lines[n]):
            n += 1
            while Block.re_excluded.match(lines[n]) is None:
inso's avatar
inso committed
329
                revokation = Revocation.from_inline(version, currency, lines[n])
inso's avatar
inso committed
330 331 332 333 334 335
                revoked.append(revokation)
                n += 1

        if Block.re_excluded.match(lines[n]):
            n += 1
            while Block.re_certifications.match(lines[n]) is None:
336 337 338 339
                exclusion_match = Block.re_exclusion.match(lines[n])
                if exclusion_match is not None:
                    exclusion = exclusion_match.group(1)
                    excluded.append(exclusion)
inso's avatar
inso committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
                n += 1

        if Block.re_certifications.match(lines[n]):
            n += 1
            while Block.re_transactions.match(lines[n]) is None:
                certification = Certification.from_inline(version, currency,
                                                          prev_hash, lines[n])
                certifications.append(certification)
                n += 1

        if Block.re_transactions.match(lines[n]):
            n += 1
            while not Block.re_hash.match(lines[n]):
                tx_lines = ""
                header_data = Transaction.re_header.match(lines[n])
                if header_data is None:
                    raise MalformedDocumentError("Compact transaction ({0})".format(lines[n]))
inso's avatar
inso committed
357
                tx_version = int(header_data.group(1))
inso's avatar
inso committed
358 359 360 361 362
                issuers_num = int(header_data.group(2))
                inputs_num = int(header_data.group(3))
                unlocks_num = int(header_data.group(4))
                outputs_num = int(header_data.group(5))
                has_comment = int(header_data.group(6))
363
                sup_lines = 2
inso's avatar
inso committed
364
                tx_max = n + sup_lines + issuers_num * 2 + inputs_num + unlocks_num + outputs_num + has_comment
inso's avatar
inso committed
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
                for i in range(n, tx_max):
                    tx_lines += lines[n]
                    n += 1
                transaction = Transaction.from_compact(currency, tx_lines)
                transactions.append(transaction)

        inner_hash = Block.parse_field("InnerHash", lines[n])
        n += 1

        noonce = int(Block.parse_field("Noonce", lines[n]))
        n += 1

        signature = Block.parse_field("Signature", lines[n])

        return cls(version, currency, number, powmin, time,
inso's avatar
inso committed
380 381
                   mediantime, ud, unit_base, issuer, issuers_frame, issuers_frame_var,
                   different_issuers_count, prev_hash, prev_issuer,
inso's avatar
inso committed
382 383 384 385
                   parameters, members_count, identities, joiners,
                   actives, leavers, revoked, excluded, certifications,
                   transactions, inner_hash, noonce, signature)

386
    def raw(self) -> str:
inso's avatar
inso committed
387 388 389 390 391 392 393 394
        doc = """Version: {version}
Type: Block
Currency: {currency}
Number: {number}
PoWMin: {powmin}
Time: {time}
MedianTime: {mediantime}
""".format(version=self.version,
395 396 397 398 399
           currency=self.currency,
           number=self.number,
           powmin=self.powmin,
           time=self.time,
           mediantime=self.mediantime)
inso's avatar
inso committed
400 401
        if self.ud:
            doc += "UniversalDividend: {0}\n".format(self.ud)
inso's avatar
inso committed
402

403
        doc += "UnitBase: {0}\n".format(self.unit_base)
inso's avatar
inso committed
404 405 406

        doc += "Issuer: {0}\n".format(self.issuer)

407
        doc += """IssuersFrame: {0}
inso's avatar
inso committed
408 409 410 411
IssuersFrameVar: {1}
DifferentIssuersCount: {2}
""".format(self.issuers_frame, self.issuers_frame_var, self.different_issuers_count)

412
        if self.number == 0 and self.parameters is not None:
413
            str_params = ":".join([str(p) for p in self.parameters])
inso's avatar
inso committed
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
            doc += "Parameters: {0}\n".format(str_params)
        else:
            doc += "PreviousHash: {0}\n\
PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer)

        doc += "MembersCount: {0}\n".format(self.members_count)

        doc += "Identities:\n"
        for identity in self.identities:
            doc += "{0}\n".format(identity.inline())

        doc += "Joiners:\n"
        for joiner in self.joiners:
            doc += "{0}\n".format(joiner.inline())

        doc += "Actives:\n"
        for active in self.actives:
            doc += "{0}\n".format(active.inline())

        doc += "Leavers:\n"
        for leaver in self.leavers:
            doc += "{0}\n".format(leaver.inline())

        doc += "Revoked:\n"
        for revokation in self.revoked:
inso's avatar
inso committed
439
            doc += "{0}\n".format(revokation.inline())
inso's avatar
inso committed
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456

        doc += "Excluded:\n"
        for exclude in self.excluded:
            doc += "{0}\n".format(exclude)

        doc += "Certifications:\n"
        for cert in self.certifications:
            doc += "{0}\n".format(cert.inline())

        doc += "Transactions:\n"
        for transaction in self.transactions:
            doc += "{0}".format(transaction.compact())

        doc += "InnerHash: {0}\n".format(self.inner_hash)

        doc += "Nonce: {0}\n".format(self.noonce)

inso's avatar
inso committed
457 458
        return doc

459
    def proof_of_work(self) -> str:
inso's avatar
inso committed
460 461 462 463 464
        doc_str = """InnerHash: {inner_hash}
Nonce: {nonce}
{signature}
""".format(inner_hash=self.inner_hash, nonce=self.noonce, signature=self.signatures[0])
        return hashlib.sha256(doc_str.encode('ascii')).hexdigest().upper()
inso's avatar
inso committed
465

466
    def computed_inner_hash(self) -> str:
467 468 469 470 471 472 473 474 475 476 477 478 479
        doc = self.signed_raw()
        inner_doc = '\n'.join(doc.split('\n')[:-2]) + '\n'
        return hashlib.sha256(inner_doc.encode("ascii")).hexdigest().upper()

    def sign(self, keys):
        """
        Sign the current document.
        Warning : current signatures will be replaced with the new ones.
        """
        key = keys[0]
        signed = self.raw()[-2:]
        signing = base64.b64encode(key.signature(bytes(signed, 'ascii')))
        self.signatures = [signing.decode("ascii")]
inso's avatar
inso committed
480

481 482
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Block):
483
            return NotImplemented
inso's avatar
inso committed
484 485
        return self.blockUID == other.blockUID

486 487 488
    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
inso's avatar
inso committed
489 490
        return self.blockUID < other.blockUID

491 492 493
    def __gt__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
inso's avatar
inso committed
494 495
        return self.blockUID > other.blockUID

496 497 498
    def __le__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
inso's avatar
inso committed
499 500
        return self.blockUID <= other.blockUID

501 502 503
    def __ge__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
inso's avatar
inso committed
504
        return self.blockUID >= other.blockUID